[
  {
    "path": ".circleci/config.yml",
    "content": "version: 2\njobs:\n  build:\n    macos:\n      xcode: '13.4.1'\n    steps:\n      - checkout\n      - run: yarn\n      - run: mkdir -p ~/reports\n      - run: yarn lint\n      - run: yarn test:ci\n      - run: yarn run dist\n      - run: mv dist/*-x64.dmg dist/Kap-x64.dmg\n      - run: mv dist/*-arm64.dmg dist/Kap-arm64.dmg\n      - store_artifacts:\n          path: dist/Kap-x64.dmg\n      - store_artifacts:\n          path: dist/Kap-arm64.dmg\n      - store_test_results:\n          path: ~/reports\n  sentry-release:\n    docker:\n      - image: cimg/node:lts\n    environment:\n      SENTRY_ORG: wulkano-l0\n      SENTRY_PROJECT: kap\n    steps:\n      - checkout\n      - run: |\n          curl -sL https://sentry.io/get-cli/ | bash\n          export SENTRY_RELEASE=$(yarn run -s sentry-version)\n          sentry-cli releases new -p $SENTRY_PROJECT $SENTRY_RELEASE\n          sentry-cli releases set-commits --auto $SENTRY_RELEASE\n          sentry-cli releases finalize $SENTRY_RELEASE\n\nworkflows:\n  version: 2\n  build:\n    jobs:\n      - build:\n          filters:\n            tags:\n              only: /.*/ # Force CircleCI to build on tags\n      - sentry-release:\n          requires:\n            - build\n          filters:\n            tags:\n              only: /^v.*/\n            branches:\n              ignore: /.*/\n"
  },
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*]\nindent_style = space\nindent_size = 2\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newline = true\n"
  },
  {
    "path": ".gitattributes",
    "content": "* text=auto eol=lf\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE.md",
    "content": "<!--\nThank you for taking the time to report an issue! ❤️\n\nBefore you continue; please make sure you've searched our existing issues to avoid duplicates. When you're ready to open a new issue include as much information as possible. You can use the handy template below for bug reports.\n\nmacOS version:        The output of `$ sw_vers`. Remember that we currently only support macOS 10.12 or later.\nKap version:          Find this in the about section of Kap, or by right-clicking on the Kap icon and pressing \"Get Info\".\nStep to reproduce:    If applicable, provide steps to reproduce the issue you're having.\nCurrent behavior:     A description of how Kap is currently behaving.\nExpected behavior:    How you expected Kap to behave.\nWorkaround:           A workaround for the issue if you've found on. (this will help others experiencing the same issue!)\n-->\n\n**macOS version:**\n**Kap version:**\n\n#### Steps to reproduce\n\n#### Current behaviour\n\n#### Expected behaviour\n\n#### Workaround\n\n<!-- If you have additional information, enter it below. -->\n"
  },
  {
    "path": ".gitignore",
    "content": "node_modules\n/renderer/out\n/renderer/.next\n/app/dist\n/dist\n/dist-js\n"
  },
  {
    "path": ".npmrc",
    "content": "package-lock=false\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.\n\n## Our Standards\n\nExamples of behavior that contributes to creating a positive environment include:\n\n* Using welcoming and inclusive language\n* Being respectful of differing viewpoints and experiences\n* Gracefully accepting constructive criticism\n* Focusing on what is best for the community\n* Showing empathy towards other community members\n\nExamples of unacceptable behavior by participants include:\n\n* The use of sexualized language or imagery and unwelcome sexual attention or advances\n* Trolling, insulting/derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or electronic address, without explicit permission\n* Other conduct which could reasonably be considered inappropriate in a professional setting\n\n## Our Responsibilities\n\nProject maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.\n\nProject maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.\n\n## Scope\n\nThis Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at hello@wulkano.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.\n\nProject maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]\n\n[homepage]: http://contributor-covenant.org\n[version]: http://contributor-covenant.org/version/1/4/\n"
  },
  {
    "path": "LICENSE.md",
    "content": "MIT License\n\nCopyright (c) Wulkano hello@wulkano.com (https://wulkano.com)\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "PRIVACY.md",
    "content": "# Your privacy\n\nEven though Kap is open-source and not in the business of monetizing the product itself nor your data, directly or indirectly, we feel it's important to share how we think about, collect and use insights we gain on how Kap is used through certain [third party services](#third-party-services).\n\nThe short version is that we respect your privacy, aim to give you as much choice and control as possible, and try to gather only the minimum amount of data absolutely necessary to help us make Kap better for you and everyone else.\n\n### Security\n\nAll our traffic is served over [HTTPS](https://en.wikipedia.org/wiki/HTTPS). Our SSL/TLS certificate is issued by [Let's Encrypt](https://letsencrypt.org). In addition to encryption, we do our best to stay vigilant and implement precautionary measures to avoid breaches or misuse.\n\n### Cookies\n\nWe do not allow any [third-party cookies](https://en.wikipedia.org/wiki/HTTP_cookie#Third-party_cookie) and do not attempt to identify you, track you or associate your data across devices and services, nor do we use cookies to serve advertising.\n\n**Authorised cookies:**\n- `_g` for Google Analytics\n- `_gat_gtag_UA_84705099_3` for Google Analytics\n- `_gid` for Google Analytics\n\nYou are of course free to block and remove cookies. You will typically find these options in the address bar of your browser.\n\n[Learn more about our use of Google Analytics](#google-analytics-gdpr-compliant).\n\n## Third-party services\n\nAn overview of third-party services that have access to, collect or generate data based on your usage.\n\n### Google Analytics (GDPR Compliant)\n\nWe do not (and do not allow third-parties to) use our Google Analytics data to track or collect personally identifiable information, nor do we (or do we allow any third-party to) associate data gathered with any personally identifying information from any source.\n\n[Google Analytics data is not shared with other Google products and services](https://support.google.com/analytics/answer/1011397), and is not accessible to Google technical support representatives or Google marketing specialists.\n\nWe do not collect or use [user specific metrics](https://support.google.com/analytics/answer/2992042), nor do we [associate data from different devices](https://support.google.com/analytics/answer/3123662).\n\nNo [Enhanced Link Attribution](https://support.google.com/analytics/answer/7377126) or [Demographics and Interests reports](https://support.google.com/analytics/answer/2799357) are used or generated, nor do we [track your search queries on our site](https://support.google.com/analytics/answer/1012264). We also do not collect data for [Display and Search Remarketing or Advertising Reporting features](https://support.google.com/analytics/answer/3450482).\n\nGoogle Analytics data for inactive sessions are retained for the [shortest currently available time](https://support.google.com/analytics/answer/7667196) (14 months). We can however retain analytics data for active sessions longer.\n\nWe ask Google Analytics to [anonymize your IP address](https://support.google.com/analytics/answer/2763052).\n\n- [Google Privacy Policy](https://policies.google.com/privacy)\n- [How Google Analytics safeguards your data](https://support.google.com/analytics/answer/6004245)\n- [Privacy Shield Certificate](https://www.privacyshield.gov/participant?id=a2zt000000001L5AAI)\n\n### Sentry (GDPR Compliant)\n\nWe prevent Sentry from storing IP Addresses, in addition to requiring enhanced privacy controls and data scrubbers be applied to prevent [sensitive data](https://docs.sentry.io/learn/sensitive-data/) being stored.\n\n- [Security & Compliance at Sentry](https://sentry.io/security/)\n- [GDPR, Sentry, and You](https://blog.sentry.io/2018/03/14/gdpr-sentry-and-you)\n- [Privacy Shield Certificate](https://www.privacyshield.gov/participant?id=a2zt0000000TNDzAAO)\n\n### MailChimp (GDPR Compliant)\n\nWe are compliant with [MailChimp Terms of Use and anti-spam requirements](https://kb.mailchimp.com/accounts/compliance-tips/terms-of-use-and-anti-spam-requirements) (CAN-SPAM).\n\nWhen you provide your name and email address to [receive email updates](http://eepurl.com/ch90_1) from us, you acknowledge that the information you provide will be transferred to MailChimp for processing in accordance with their [Privacy Policy](https://mailchimp.com/legal/privacy/) and [Terms](https://mailchimp.com/legal/terms/).\n\nWe do not allow MailChimp to use your information in their data science projects.\n\nYou can unsubscribe from emails regarding Kap at any time by using the unsubscribe link in the footer of any email you receive from us, or by contacting us at hello@wulkano.com.\n\n- [About MailChimp, the EU/Swiss Privacy Shield, and the GDPR](https://kb.mailchimp.com/accounts/management/about-mailchimp-the-eu-swiss-privacy-shield-and-the-gdpr)\n- [Privacy Shield Certificate](https://www.privacyshield.gov/participant?id=a2zt0000000TO6hAAG)\n\n## Help us do even better\n\nWe'd love to hear from you! [Open an issue](https://github.com/wulkano/kap/issues/new) or [send an email](mailto:hello@wulkano.com) to help shape and strengthen our stance on privacy.\n\n*Updated May 24, 2018*\n"
  },
  {
    "path": "README.md",
    "content": "<p align=\"center\">\n  <img src=\"https://getkap.co/static/favicon/kap.svg\" height=\"64\">\n  <h3 align=\"center\">Kap</h3>\n  <p align=\"center\">An open-source screen recorder built with web technology<p>\n  <p align=\"center\"><a href=\"https://circleci.com/gh/wulkano/kap\"><img src=\"https://circleci.com/gh/wulkano/Kap.svg?style=shield\" alt=\"Build Status\"></a> <a href=\"https://github.com/sindresorhus/xo\"><img src=\"https://img.shields.io/badge/code_style-XO-5ed9c7.svg\" alt=\"XO code style\"></a></p>\n</p>\n\n[![SWUbanner](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/banner2-direct.svg)](https://vshymanskyy.github.io/StandWithUkraine/)\n\n## Get Kap\n\nDownload the latest release:\n\n- [Apple silicon](https://getkap.co/api/download/arm64)\n- [Intel](https://getkap.co/api/download/x64)\n\nOr install with [Homebrew-Cask](https://caskroom.github.io):\n\n```sh\nbrew install --cask kap\n```\n\n## How To Use Kap\n\nClick the menu bar icon to bring up the screen recorder. After selecting what portion of the screen you'd like to record, hit the record button to start recording. Click the menu bar icon again to stop the recording.\n\n> Tip: While recording, Option-click the menu bar icon to pause or right-click for more options.\n\n## Contribute\n\nRead the [contribution guide](contributing.md).\n\n## Plugins\n\nFor more info on how to create plugins, read the [plugins docs](docs/plugins.md).\n\n## Dev builds\n\nDownload [`main`](https://kap-artifacts.now.sh/main) or builds for any other branch using: `https://kap-artifacts.now.sh/<branch>`. Note that these builds are unsupported and may have issues.\n\n## Related Repositories\n\n- [Website](https://github.com/wulkano/kap-website)\n- [Aperture](https://github.com/wulkano/aperture)\n\n## Newsletter\n\n[Subscribe](http://eepurl.com/ch90_1)\n\n## Thanks\n\n- [▲ Vercel](https://vercel.com/) for fast deployments served from the edge, hosting our website, downloads, and updates.\n- [● CircleCI](https://circleci.com/) for supporting the open source community and making our builds fast and reliable.\n- [△ Sentry](https://sentry.io/) for letting us know when Kap isn't behaving and helping us eradicate said behaviour.\n- Our [contributors](https://github.com/wulkano/kap/contributors) who help maintain Kap and make screen recording and sharing easy.\n"
  },
  {
    "path": "build/entitlements.mac.inherit.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n  <dict>\n    <key>com.apple.security.cs.allow-jit</key>\n    <true/>\n    <key>com.apple.security.cs.allow-unsigned-executable-memory</key>\n    <true/>\n    <key>com.apple.security.cs.allow-dyld-environment-variables</key>\n    <true/>\n    <key>com.apple.security.device.audio-input</key>\n    <true/>\n    <key>com.apple.security.device.camera</key>\n    <true/>\n  </dict>\n</plist>\n"
  },
  {
    "path": "contributing.md",
    "content": "# Contributing\n\n1. [Fork](https://help.github.com/articles/fork-a-repo/) this repository to your own GitHub account and then [clone](https://help.github.com/articles/cloning-a-repository/) it to your local device\n2. Install the dependencies: `yarn`\n3. Build the code, start the app, and watch for changes: `yarn start`\n\nTo make sure that your code works in the finished app, you can generate the binary:\n\n```\n$ yarn run pack\n```\n\nAfter that, you'll see the binary in the `dist` folder 😀\n"
  },
  {
    "path": "docs/plugins.md",
    "content": "# Plugins\n\nThe Kap plugin system lets you create custom share targets that appear in the editor export menu. You could, for example, create a plugin to share a screen recording on YouTube.\n\nYou can discover plugins or view installed ones by clicking the `Kap` menu, `Preferences…`, and selecting the `Plugins` pane.\n\n## Getting started\n\nA Kap plugin is an npm package that exports one or more services. The plugin runs in the main Electron process (Node.js). That means you can use any npm package in your plugin. Kap plugins are published to npm, like any other npm package.\n\nTake a look at existing plugin examples in each section to see how they work.\n\nTip: You can use modern JavaScript features like async/await in your plugin.\n\n## Requirements\n\n- Your package must be named with the `kap-` prefix. For example `kap-giphy`.\n- You must have the `kap-plugin` keyword in package.json. Add additional relevant keywords to improve discovery.\n- The `\"description\"` in package.json should succinctly describe what you can do with it. For example: `Share GIFs on GIPHY`. Not something like this: `Kap plugin that uploads GIFs to GIPHY`.\n- The readme should follow the style of [`kap-giphy`](https://github.com/wulkano/kap-giphy).\n- Your plugin must be tested, preferably using [`kap-plugin-test`](https://github.com/SamVerschueren/kap-plugin-test) and [`kap-plugin-mock-context`](https://github.com/samverschueren/kap-plugin-mock-context). [Example](https://github.com/wulkano/kap-giphy/blob/main/test/test.js).\n- The package.json file can include a `kap` object with the following options:\n    - `version`: a [semver range](https://nodesource.com/blog/semver-a-primer/) of the Kap versions your plugin supports. Defaults to `*`.\n    - `macosVersion`: a [semver range](https://nodesource.com/blog/semver-a-primer/) of the macOS versions your plugin supports. Defaults to `*`.\n\n- **Deprecation notice:** If your plugin only supports specific versions of Kap, include a `kapVersion` field in the package.json with a [semver range](https://nodesource.com/blog/semver-a-primer/). This is still supported but will be removed at some point in favor of `kap.version`.\n\n## Development\n\nWhen you develop a plugin it’s useful to be able to try it out in Kap. In the directory of your plugin, run `$ npm link`, go to `~/Library/Application Support/Kap/plugins` and run `$ npm link plugin-name `, and then add `\"plugin-name\": \"latest\"` to the `\"dependencies\"` in the package.json there. Your plugin should now be shown in Kap.\n\nWhen Kap is built for production, it prunes dependencies at launch time. In order to avoid any issues, make sure to run `$ npm link` after launching Kap, and make sure to re-run it if you restart Kap. Alternatively, you can run Kap in dev mode by downloading the source and running `$ yarn start`.\n\n## Services\n\nKap currently supports three different types of services and a plugin can have multiple of each, although each plugin should focus on a specific area.\n\n### Share services\n\nA share service lets you add an entry to the export menu in the Kap editor window and control what happens when the user clicks it.\n\n<img src=\"https://cloud.githubusercontent.com/assets/170270/26560296/8ac42740-44df-11e7-88f5-46f8483ffea1.jpg\" width=\"1024\">\n\n```\n| Save to Disk      |\n| Upload to Dropbox |\n| Share on GIPHY    |\n```\n\nIn the above case, the second and third item are added by two different share services.\n\nThe share service is a plain object defining some metadata:\n\n- `title`: The title used in the export menu. For example: `Share to GIPHY`.<br>The text should be in [title case](https://capitalizemytitle.com), for example, `Save to Disk`, not `Save to disk`.\n- `configDescription`: A description displayed at the top of the configuration window. You can use this to explain the config options or link to API docs. Any links in this description will be parsed into clickable links automatically.\n- `formats`: The file formats you support. Can be: `gif`, `mp4`, `webm`, `apng`\n- `action`: The function that is run when the user clicks the menu item. [Read more below.](#action)\n- `config`: Definition of the config the plugins needs. [Read more below](#config).\n\nThe `config` and `configDescription` properties are optional.\n\nExample:\n\n```js\nconst action = async context => {\n  // Do something\n\n  context.notify('Notify about something');\n};\n\nconst config = {\n  apiKey: {\n    title: 'API key',\n    type: 'string',\n    minLength: 13,\n    default: '',\n    required: true\n  }\n};\n\nconst giphy = {\n  title: 'Share to GIPHY',\n  formats: [\n    'gif'\n  ],\n  action,\n  config\n};\n\nexports.shareServices = [giphy];\n```\n\n#### Action\n\nThe `action` function is where you implement the behavior of your service. The function receives a `context` argument with some metadata and utility methods.\n\n- `.format`: The file format the user chose in the editor window. Can be: `gif`, `mp4`, `webm`, `apng`\n- `.prettyFormat`: Prettified version of `.format` for use in notifications. Can be: `GIF`, `MP4`, `WebM`, `APNG`\n- `.defaultFileName`: Default file name for the recording. For example: `Kapture 2017-05-30 at 1.03.49.gif`\n- `.filePath()`: Convert the screen recording to the user chosen format and return a Promise for the file path.\n  - If you want to overwrite the format that the user selected, you can pass a `fileType` option: `.filePath({fileType: 'mp4'})`. It can be one of `mp4`, `gif`, `apng`, `webm`. This can be useful if you, for example, need to handle the GIF conversion yourself.\n- `.config`: Get and set config for your plugin. It’s an instance of [`electron-store`](https://github.com/sindresorhus/electron-store#instance).\n- `.request()`: Do a network request, like uploading. It’s a wrapper around [`got`](https://github.com/sindresorhus/got).\n- `.copyToClipboard(text)`: Copy text to the clipboard. If you for example copy a link to the uploaded recording to the clipboard, don’t forget to `.notify()` the user about it.\n- `.notify(text, action)`: Show a notification. Optionally pass in a function that is called with the event when the notification is clicked.\n- `.setProgress(text, percentage)`: Update progress information in the Kap export window. Use this whenever you have long-running jobs, like uploading. The `percentage` should be a number between `0` and `1`.\n- `.openConfigFile()`: Open the plugin config file in the user’s editor.\n- `.cancel()`: Indicate that the plugin operation canceled for some reason. [Example.](https://github.com/wulkano/kap/blob/efc32d12f381615c9fcfc41065d9c2ee200e8975/app/src/main/save-file-service.js#L28-L31) If the cancelation was not the result of a user gesture, use `.notify()` to inform the user why it was canceled.\n- `.waitForDeepLink()`: Returns a Promise that resolves when a deep link for this plugin is opened. The link should be in the format `kap://plugins/{pluginName}/{rest}`, where `pluginName` is the npm package name and `rest` is the string the Promise will resolve with. This is useful for [OAuth flows](#oauth).\n\n#### Notes\n\nUse `context.setProgress()` whenever possible to keep the user updated on what's happening. The `.filePath()` method sets its own progress, so you should not do it for that step.\n\nExample plugins: [`kap-giphy`](https://github.com/wulkano/kap-giphy/blob/main/index.js), [`kap-s3`](https://github.com/SamVerschueren/kap-s3), [`kap-imgur`](https://github.com/kevva/kap-imgur), [`kap-streamable`](https://github.com/kevva/kap-streamable)\n\n### Edit services\n\nOnly supported in Kap >= 3.2.0.\n\nAn edit service lets you add an entry to the edit menu in the Kap editor window and process the recording before it gets converted and exported. The edit service receives an `mp4` file which is generated from the recording after trimming the duration and adjusting the size. It's expected to produce another `mp4` file at the given output location, which will then be passed to the appropriate share service.\n\nThe edit service is a plain object defining some metadata:\n\n- `title`: The title used in the export menu. For example: `Reverse`.\\\n  The text should be in [title case](https://capitalizemytitle.com), for example, `Slow Down`, not `Slow down`.\n- `configDescription`: A description displayed at the top of the configuration window. You can use this to explain the config options or link to API docs. Any links in this description will be parsed into clickable links automatically.\n- `action`: The function that is run when the user clicks the menu item. [Read more below.](#action)\n- `config`: Definition of the config the plugins needs. [Read more below](#config).\n\nThe `config` and `configDescription` properties are optional.\n\nExample:\n\n```js\nconst action = async context => {\n  // Do something\n\n  context.notify('Notify about something');\n};\n\nconst config = {\n  percent: {\n    title: 'Slow Down Percentage',\n  type: 'number',\n  maximum: 1,\n  minimum: 0,\n    default: 0.5,\n    required: true\n  }\n};\n\nconst slowDown = {\n  title: 'Slow Down',\n  action,\n  config\n};\n\nexports.editServices = [slowDown];\n```\n\n#### Action\n\nThe `action` function is where you implement the behavior of your service. The function receives a `context` argument with some metadata and utility methods.\n\n- `.inputPath`: The path to the input trimmed `mp4` file.\n- `.outputPath`: The path where the resulting `mp4` file should be by the end of the action.\n- `.exportOptions`: An object containing info about the recording (note that the input video has already been resized and trimmed):\n  - `.width`: Width of the input file.\n  - `.height`: Height of the input file.\n  - `.format`: The selected format in which the video will be converted to later on.\n  - `.fps`: The selected FPS that will be used for the final conversion.\n  - `.duration`: Duration of the trimmed input file.\n  - `.isMuted`: Whether the video is muted or not.\n  - `.loop`: Whether the resulting GIF or APNG file will be looped or not.\n- `.convert(args, text)`: A utility function which accepts an array of `ffmpeg` arguments and handles executing the command, parsing the progress, generating time estimate and showing it to the user. The second argument is optional and defaults to `Converting`. It can be something more descriptive to your service like `Reversing` and will be used for the status reporting.\n\nExample (reversing a video):\n\n```js\nconst reverseAction = async context => {\n  return context.convert([\n    '-i',\n    context.inputPath,\n    '-vf', 'reverse',\n    context.outputPath\n  ], 'Reversing');\n\n  // Will call ffmpeg -i {inputPath} -vf reverse {outputPath}\n};\n```\n- `.config`: Get and set config for your plugin. It’s an instance of [`electron-store`](https://github.com/sindresorhus/electron-store#instance).\n- `.request()`: Do a network request, like uploading. It’s a wrapper around [`got`](https://github.com/sindresorhus/got).\n- `.copyToClipboard(text)`: Copy text to the clipboard. If you for example copy a link to the uploaded recording to the clipboard, don’t forget to `.notify()` the user about it.\n- `.notify(text, action)`: Show a notification. Optionally pass in a function that is called with the event when the notification is clicked.\n- `.openConfigFile()`: Open the plugin config file in the user’s editor.\n- `.cancel()`: Indicate that the plugin operation canceled for some reason. [Example.](https://github.com/wulkano/kap/blob/efc32d12f381615c9fcfc41065d9c2ee200e8975/app/src/main/save-file-service.js#L28-L31) If the cancelation was not the result of a user gesture, use `.notify()` to inform the user why it was canceled.\n- `.waitForDeepLink()`: Returns a Promise that resolves when a deep link for this plugin is opened. The link should be in the format `kap://plugins/{pluginName}/{rest}`, where `pluginName` is the npm package name and `rest` is the string the Promise will resolve with. This is useful for [OAuth flows](#oauth).\n\n#### Notes\n\nIt is highly recomended that an edit service uses a [PCancelable](https://github.com/sindresorhus/p-cancelable) function as the action, so Kap can cancel it in case the user decides to cancel the export.\n\nExample:\n\n```js\nconst PCancelable = require('p-cancelable');\n\nconst action = PCancelable.fn(async (context, onCancel) => {\n  const process = context.convert([\n    '-i',\n    context.inputPath,\n    '-vf', 'reverse',\n    context.outputPath\n  ], 'Reversing');\n\n  onCancel(() => {\n    process.cancel();\n  });\n\n  await process;\n});\n```\n\nExample plugins: [`kap-playback-speed`](https://github.com/karaggeorge/kap-playback-speed), [`kap-reverse`](https://github.com/karaggeorge/kap-reverse)\n\n### Record services\n\nA record service lets you add an entry to the “Plugins” submenu of the main context menu of the cropper. A user can enable or disable the service and when enabled, the service can take action in different stages of the recording process.\n\nRecord services are different from share and edit services, since they don't have one action but many hooks.\n\nThe record service is a plain object defining some metadata and hooks:\n\n- `title`: The title used in the export menu. For example: `Share to GIPHY`.\\\n  The text should be in [title case](https://capitalizemytitle.com), for example, `Save to Disk`, not `Save to disk`.\n- `configDescription`: A description displayed at the top of the configuration window. You can use this to explain the config options or link to API docs. Any links in this description will be parsed into clickable links automatically.\n- `config`: Definition of the config the plugins needs. [Read more below](#config).\n- `willStartRecording`: Function that is called before the recording starts. [Read more below.](#hooks)\n- `didStartRecording`: Function that is called after the recording starts. [Read more below.](#hooks)\n- `didStopRecording`: Function that is called after the recording stops. [Read more below.](#hooks)\n- `willEnable`: Function that is called when the user enables the service. [Read more below.](#hooks)\n- `cleanUp`: Function that is called if Kap exited unexpectedly last time it was run (for example, if it crashed), without the `didStopRecording` hook being called. This hook will only receive the `persistedState` object from the `state` passed to the rest of the hooks. Use this to clean up any effects introduced when the recording started and don't automatically clear out once Kap stops. For example, if your plugin killed a running app with intent to restart it after the recording was over, you can use `cleanUp` to ensure the app is properly restarted even in the event that Kap crashed, so the `didStopRecording` wasn't called.\n\nThe `config`, `configDescription` and hook properties are optional.\n\nExample:\n\n```js\nconst willStartRecording = async context => {\n  // Do something\n  context.notify('Recording will start now!');\n};\n\nconst didStopRecording = async context => {\n  // Do something\n  context.notify('Recording stopped!');\n};\n\nconst config = {\n  apiKey: {\n    title: 'API Key',\n    type: 'string',\n    minLength: 13,\n    default: '',\n    required: true\n  }\n};\n\nconst doNotDisturb = {\n  title: 'Silence Notifications',\n  willStartRecording,\n  didStopRecording,\n  config\n};\n\nexports.recordServices = [doNotDisturb];\n```\n\n#### Hooks\n\nEach hook is called as described above. Each function can be asynchronous and will be called with a context object described below. The only hook that behaves differently is `willEnable`. This hook will be called when a service is about to be enabled (including after installing the plugin if the config is valid). The hook can be an asynchronous function and if it returns or resolves with `false`, the hook will not be enabled.\n\nYou can use this to check if you have enough permissions for the service to work, and if not you can request the missing permissions and return `false`. This ensures that your other plugin hooks will not be called until `willEnable` returns `true`.\n\n#### Hooks Context\n\nThe hook functions receive a `context` argument with some metadata and utility methods.\n\n- `.state`: An object that will be shared and passed to all hooks in the same recording process. It can be useful to persist data between the different hooks.\n    - `state.persistedState`: An object under `state` which should only contain serializable fields. It will be passed to the `cleanUp` hook if Kap didn't shut down correctly last time it was run. Use this to store fields necessary to clean up remaining effects.\n- `.apertureOptions`: An object with the options passed to [Aperture](https://github.com/wulkano/aperture-node). The API is described [here](https://github.com/wulkano/aperture-node#options).\n- `.config`: Get and set config for your plugin. It’s an instance of [`electron-store`](https://github.com/sindresorhus/electron-store#instance).\n- `.request()`: Do a network request, like uploading. It’s a wrapper around [`got`](https://github.com/sindresorhus/got).\n- `.copyToClipboard(text)`: Copy text to the clipboard. If you for example copy a link to the uploaded recording to the clipboard, don’t forget to `.notify()` the user about it.\n- `.notify(text, action)`: Show a notification. Optionally pass in a function that is called with the event when the notification is clicked.\n- `.openConfigFile()`: Open the plugin config file in the user’s editor.\n- `.cancel()`: Indicate that the plugin operation canceled for some reason. [Example.](https://github.com/wulkano/kap/blob/efc32d12f381615c9fcfc41065d9c2ee200e8975/app/src/main/save-file-service.js#L28-L31) If the cancelation was not the result of a user gesture, use `.notify()` to inform the user why it was canceled.\n- `.waitForDeepLink()`: Returns a Promise that resolves when a deep link for this plugin is opened. The link should be in the format `kap://plugins/{pluginName}/{rest}`, where `pluginName` is the npm package name and `rest` is the string the Promise will resolve with. This is useful for [OAuth flows](#oauth).\n\nExample plugins: [`kap-do-not-disturb`](https://github.com/karaggeorge/kap-do-not-disturb), [`kap-hide-desktop-icons`](https://github.com/karaggeorge/kap-hide-desktop-icons)\n\n## Config\n\nThe config system uses [JSON Schema](http://json-schema.org) which lets you describe the config your plugin supports and have it validated and enforced. For example, you can define that some config key is required, or that it should be a string with a minimum length of 10. Kap will notify the user of invalid config. If you define required config, Kap will open the config file automatically on install so the user can fill out the required fields.\n\nIt’s recommended to set an empty `default` property for required config keys, so the user can just fill them out.\n\nThe `title` property must be defined for each config key. *(We’ll use it in the future to render your config directly in the UI)*\n\nExample:\n\n```js\nconfig: {\n  username: {\n    title: 'Username',\n    type: 'string',\n    minLength: 5,\n    default: '',\n    required: true\n  },\n  hasUnicorn: {\n    title: 'Do you have a unicorn?',\n    type: 'boolean',\n    default: false,\n    required: true\n  }\n}\n```\n\n[Read more about JSON Schema](https://spacetelescope.github.io/understanding-json-schema/)\n\n### Custom Types\n\nKap offers a few custom types which can be displayed in a better way to the user.\n\n#### `hexColor`\n\n```js\nconst config = {\n  barColor: {\n    title: 'Color',\n    customType: 'hexColor',\n    required: true,\n    default: '#007aff'\n  }\n};\n```\n\n<img src=\"../media/plugins/hexColor.png\" width=\"319\">\n\n#### `keyboardShortcut`\n\n[List of possible values](https://www.electronjs.org/docs/api/accelerator)\n\n```js\nconst config = {\n  keyboardShortcut: {\n    title: 'Toggle Unicorn Mode',\n    customType: 'keyboardShortcut',\n    required: true,\n    default: 'Command+Shift+5'\n  }\n};\n\n// Later\n\nelectron.globalShortcut.register(config.get('shortcut'), () => { /* ... */ });\n```\n\n**Note:** Kap will not register any action for the keyboard shortcut. That is up to the plugin implementation.\n\n## General APIs\n\nEvery type of plugin and service can additionally export the following:\n- `didInstall(config)`: A hook that will be called when the plugin is first installed.\n- `didConfigChange(newValues, oldValues, config)`: A hook that will be called whenever the config of the plugin is changed.\n- `willUninstall(config)`: A hook that will be called when a plugin is being uninstalled. It can be used to clean up artifacts.\n\nIn addition to these, each plugin needs to export at least one of the following:\n- `shareServices`: an array of share services and described above\n- `editServices`: an array of edit services and described above\n- `recordServices`: an array of record services and described above\n\n## OAuth\n\nSometimes services require an [OAuth](https://oauth.net/2/) flow to retrieve a token. These flows are often required to be completed in the browser and not in a webview. For this reason, Kap provides deep linking support. Follow these steps to support OAuth in your plugin:\n\n- When the export starts, check if the `accessToken` is available (if you have already authenticated) in the plugin config. This should not be listed in the JSON Schema options mentioned above unless the user is meant to edit them.\n- If it's not available, [open an external link](https://www.electronjs.org/docs/api/shell#shellopenexternalurl-options) to the OAuth provider's page with the correct parameters. Usually client ID.\n- When registering the app, provide something like `kap://plugins/{pluginName}/auth` as the callback URL.\n- Call `context.waitForDeepLink()` and wait for the user to go through the process.\n- When the above call resolves, you'll have the remaining path, along with any extra info the API added. In the above example, it would be something like `auth?code=###`.\n- You can now exchange the code for a token, and then store that in the config, so you can use it for future exports.\n\nFor an example of this flow in action, check out [kap-dropbox](https://github.com/karaggeorge/kap-dropbox).\n\nIf the API provider only allows HTTP/HTTPS URLs, or if you don't want to do the code exchange in the plugin (to avoid having the secret in the code), you might need to create a proxy, similar to the [one used for kap-dropbox](https://github.com/karaggeorge/kap-dropbox/tree/main/oauth-proxy), to trigger the deep link.\n\n## Removing your Kap plugin\n\nSince npm doesn't allow you to remove packages from the registry, Kap filters out deprecated packages in the plugin list.\n\nWhen you are ready to retire your Kap plugin, simply run `npm deprecate kap-plugin \"Deprecated\"`.\n\n[Read more about the `npm-deprecate` command](https://docs.npmjs.com/cli/deprecate)\n"
  },
  {
    "path": "main/aperture.ts",
    "content": "import {windowManager} from './windows/manager';\nimport {setRecordingTray, setPausedTray, disableTray, resetTray} from './tray';\nimport {setCropperShortcutAction} from './global-accelerators';\nimport {settings} from './common/settings';\nimport {track} from './common/analytics';\nimport {plugins} from './plugins';\nimport {getAudioDevices, getSelectedInputDeviceId} from './utils/devices';\nimport {showError} from './utils/errors';\nimport {RecordServiceContext, RecordServiceState} from './plugins/service-context';\nimport {setCurrentRecording, updatePluginState, stopCurrentRecording} from './recording-history';\nimport {Recording} from './video';\nimport {ApertureOptions, StartRecordingOptions} from './common/types';\nimport {InstalledPlugin} from './plugins/plugin';\nimport {RecordService, RecordServiceHook} from './plugins/service';\nimport {getCurrentDurationStart, getOverallDuration, setCurrentDurationStart, setOverallDuration} from './utils/track-duration';\n\nconst createAperture = require('aperture');\nconst aperture = createAperture();\n\nlet recordingPlugins: Array<{plugin: InstalledPlugin; service: RecordService}> = [];\nconst serviceState = new Map<string, RecordServiceState>();\nlet apertureOptions: ApertureOptions;\nlet recordingName: string | undefined;\nlet past: number | undefined;\n\nconst setRecordingName = (name: string) => {\n  recordingName = name;\n};\n\nconst serializeEditPluginState = () => {\n  const result: Record<string, Record<string, Record<string, unknown> | undefined>> = {};\n\n  for (const {plugin, service} of recordingPlugins) {\n    if (!result[plugin.name]) {\n      result[plugin.name] = {};\n    }\n\n    result[plugin.name][service.title] = serviceState.get(service.title)?.persistedState;\n  }\n\n  return result;\n};\n\nconst callPlugins = async (method: RecordServiceHook) => Promise.all(recordingPlugins.map(async ({plugin, service}) => {\n  if (service[method] && typeof service[method] === 'function') {\n    try {\n      await service[method]?.(\n        new RecordServiceContext({\n          plugin,\n          apertureOptions,\n          state: serviceState.get(service.title) ?? {},\n          setRecordingName\n        })\n      );\n    } catch (error) {\n      showError(error as any, {title: `Something went wrong while using the plugin “${plugin.prettyName}”`, plugin});\n    }\n  }\n}));\n\nconst cleanup = async () => {\n  windowManager.cropper?.close();\n  resetTray();\n\n  await callPlugins('didStopRecording');\n  serviceState.clear();\n\n  setCropperShortcutAction();\n};\n\nexport const startRecording = async (options: StartRecordingOptions) => {\n  if (past) {\n    return;\n  }\n\n  past = Date.now();\n  recordingName = undefined;\n\n  windowManager.preferences?.close();\n  windowManager.cropper?.disable();\n  disableTray();\n\n  const {cropperBounds, screenBounds, displayId} = options;\n\n  cropperBounds.y = screenBounds.height - (cropperBounds.y + cropperBounds.height);\n\n  const {\n    record60fps,\n    showCursor,\n    highlightClicks,\n    recordAudio\n  } = settings.store;\n\n  apertureOptions = {\n    fps: record60fps ? 60 : 30,\n    cropArea: cropperBounds,\n    showCursor,\n    highlightClicks,\n    screenId: displayId\n  };\n\n  if (recordAudio) {\n    // In case for some reason the default audio device is not set\n    // use the first available device for recording\n    const audioInputDeviceId = getSelectedInputDeviceId();\n    if (audioInputDeviceId) {\n      apertureOptions.audioDeviceId = audioInputDeviceId;\n    } else {\n      const [defaultAudioDevice] = await getAudioDevices();\n      apertureOptions.audioDeviceId = defaultAudioDevice?.id;\n    }\n  }\n\n  // TODO: figure out how to correctly process hevc videos with ffmpeg\n  // if (recordHevc) {\n  //   apertureOptions.videoCodec = 'hevc';\n  // }\n\n  console.log(`Collected settings after ${(Date.now() - past) / 1000}s`);\n\n  recordingPlugins = plugins\n    .recordingPlugins\n    .flatMap(\n      plugin => {\n        const validServices = plugin.config.validServices;\n        return plugin.recordServicesWithStatus\n          // Make sure service is valid and enabled\n          .filter(({title, isEnabled}) => isEnabled && validServices.includes(title))\n          .map(service => ({plugin, service}));\n      }\n    );\n\n  for (const {service, plugin} of recordingPlugins) {\n    serviceState.set(service.title, {persistedState: {}});\n    track(`plugins/used/record/${plugin.name}`);\n  }\n\n  await callPlugins('willStartRecording');\n\n  try {\n    const filePath = await aperture.startRecording(apertureOptions);\n    setOverallDuration(0);\n    setCurrentDurationStart(Date.now());\n\n    setCurrentRecording({\n      filePath,\n      name: recordingName,\n      apertureOptions,\n      plugins: serializeEditPluginState()\n    });\n  } catch (error) {\n    track('recording/stopped/error');\n    showError(error as any, {title: 'Recording error', plugin: undefined});\n    past = undefined;\n    cleanup();\n    return;\n  }\n\n  const startTime = (Date.now() - past) / 1000;\n  if (startTime > 3) {\n    track(`recording/started/${startTime}`);\n  } else {\n    track('recording/started');\n  }\n\n  console.log(`Started recording after ${startTime}s`);\n  windowManager.cropper?.setRecording();\n  setRecordingTray();\n  setCropperShortcutAction(stopRecording);\n  past = Date.now();\n\n  // Track aperture errors after recording has started, to avoid kap freezing if something goes wrong\n  aperture.recorder.catch((error: any) => {\n    // Make sure it doesn't catch the error of ending the recording\n    if (past) {\n      track('recording/stopped/error');\n      showError(error, {title: 'Recording error', plugin: undefined});\n      past = undefined;\n      cleanup();\n    }\n  });\n\n  await callPlugins('didStartRecording');\n  updatePluginState(serializeEditPluginState());\n};\n\nexport const stopRecording = async () => {\n  // Ensure we only stop recording once\n  if (!past) {\n    return;\n  }\n\n  console.log(`Stopped recording after ${(Date.now() - past) / 1000}s`);\n  past = undefined;\n\n  let filePath;\n\n  try {\n    filePath = await aperture.stopRecording();\n    setOverallDuration(0);\n    setCurrentDurationStart(0);\n  } catch (error) {\n    track('recording/stopped/error');\n    showError(error as any, {title: 'Recording error', plugin: undefined});\n    cleanup();\n    return;\n  }\n\n  try {\n    cleanup();\n  } finally {\n    track('editor/opened/recording');\n\n    const recording = new Recording({\n      filePath,\n      title: recordingName,\n      apertureOptions\n    });\n    await recording.openEditorWindow();\n\n    stopCurrentRecording(recordingName);\n  }\n};\n\nexport const stopRecordingWithNoEdit = async () => {\n  // Ensure we only stop recording once\n  if (!past) {\n    return;\n  }\n\n  console.log(`Stopped recording after ${(Date.now() - past) / 1000}s`);\n  past = undefined;\n\n  try {\n    await aperture.stopRecording();\n    setOverallDuration(0);\n    setCurrentDurationStart(0);\n  } catch (error) {\n    track('recording/quit/error');\n    showError(error as any, {title: 'Recording error', plugin: undefined});\n    cleanup();\n    return;\n  }\n\n  try {\n    cleanup();\n  } finally {\n    track('recording/quit');\n    stopCurrentRecording(recordingName);\n  }\n};\n\nexport const pauseRecording = async () => {\n  // Ensure we only pause if there's a recording in progress and if it's currently not paused\n  const isPaused = await aperture.isPaused();\n  if (!past || isPaused) {\n    return;\n  }\n\n  try {\n    await aperture.pause();\n    setOverallDuration(getOverallDuration() + (Date.now() - getCurrentDurationStart()));\n    setCurrentDurationStart(0);\n    setPausedTray();\n    track('recording/paused');\n    console.log(`Paused recording after ${(Date.now() - past) / 1000}s`);\n  } catch (error) {\n    track('recording/paused/error');\n    showError(error as any, {title: 'Recording error', plugin: undefined});\n    cleanup();\n  }\n};\n\nexport const resumeRecording = async () => {\n  // Ensure we only resume if there's a recording in progress and if it's currently paused\n  const isPaused = await aperture.isPaused();\n  if (!past || !isPaused) {\n    return;\n  }\n\n  try {\n    await aperture.resume();\n    setCurrentDurationStart(Date.now());\n    setRecordingTray();\n    track('recording/resumed');\n    console.log(`Resume recording after ${(Date.now() - past) / 1000}s`);\n  } catch (error) {\n    track('recording/resumed/error');\n    showError(error as any, {title: 'Recording error', plugin: undefined});\n    cleanup();\n  }\n};\n"
  },
  {
    "path": "main/common/accelerator-validator.ts",
    "content": "// The goal of this file is validating accelerator values we receive from the user\n// to make sure that they are can be used with the electron api https://www.electronjs.org/docs/api/accelerator\n\n// Also, this extracts the right accelerator from a keyboard event, checking the\n// location for numpad keys and special characters for when shift is pressed\n\nconst modifiers = ['Command', 'Alt', 'Option', 'Shift', 'Cmd', 'Control', 'Ctrl'];\n\nconst codes = [\n  'Plus',\n  'Space',\n  'Tab',\n  'Capslock',\n  'Numlock',\n  'Scrolllock',\n  'Backspace',\n  'Delete',\n  'Insert',\n  'Return',\n  'Enter',\n  'Up',\n  'Down',\n  'Left',\n  'Right',\n  'PageUp',\n  'PageDown',\n  'Escape',\n  'Esc',\n  'VolumeUp',\n  'VolumeDown',\n  'VolumeMute',\n  'num0',\n  'num1',\n  'num2',\n  'num3',\n  'num4',\n  'num5',\n  'num6',\n  'num7',\n  'num8',\n  'num9',\n  'numdec',\n  'numadd',\n  'numsub',\n  'nummult',\n  'numdiv'\n] as const;\n\nconst getKeyCodeRegex = () => new RegExp('^([\\\\dA-Z~`!@#$%^&*()_+=.,<>?;:\\'\"\\\\-\\\\/\\\\\\\\\\\\[\\\\]\\\\{\\\\}\\\\|]|F([1-9]|1[\\\\d]|2[0-4])|' + codes.join('|') + ')$');\n\nconst shiftKeyMap = new Map([\n  ['~', '`'],\n  ['!', '1'],\n  ['@', '2'],\n  ['#', '3'],\n  ['$', '4'],\n  ['%', '5'],\n  ['^', '6'],\n  ['&', '7'],\n  ['*', '8'],\n  ['(', '9'],\n  [')', '0'],\n  ['_', '-'],\n  ['+', '='],\n  ['{', '['],\n  ['}', ']'],\n  ['|', '\\\\'],\n  [':', ';'],\n  ['\"', '\\''],\n  ['<', ','],\n  ['>', '.'],\n  ['?', '/']\n]);\n\nconst numpadKeyMap = new Map([\n  ['0', 'num0'],\n  ['1', 'num1'],\n  ['2', 'num2'],\n  ['3', 'num3'],\n  ['4', 'num4'],\n  ['5', 'num5'],\n  ['6', 'num6'],\n  ['7', 'num7'],\n  ['8', 'num8'],\n  ['9', 'num9'],\n  ['.', 'numdec'],\n  ['+', 'numadd'],\n  ['-', 'numsub'],\n  ['*', 'nummult'],\n  ['/', 'numdiv']\n]);\n\nconst namedKeyCodeMap = new Map([\n  [' ', 'Space'],\n  ['CapsLock', 'Capslock'],\n  ['ArrowUp', 'Up'],\n  ['ArrowDown', 'Down'],\n  ['ArrowLeft', 'Left'],\n  ['ArrowRight', 'Right'],\n  ['Clear', 'Numlock']\n]);\n\nexport const checkAccelerator = (accelerator: string) => {\n  if (!accelerator) {\n    return true;\n  }\n\n  const parts = accelerator.split('+');\n\n  if (parts.length < 2) {\n    return false;\n  }\n\n  if (!getKeyCodeRegex().test(parts[parts.length - 1])) {\n    return false;\n  }\n\n  const metaKeys = parts.slice(0, -1);\n  return metaKeys.every(part => modifiers.includes(part)) && metaKeys.some(part => part !== 'Shift');\n};\n\nexport const eventKeyToAccelerator = (key: string, location: number) => {\n  if (location === 3) {\n    return numpadKeyMap.get(key);\n  }\n\n  return namedKeyCodeMap.get(key) ?? shiftKeyMap.get(key) ?? key.toUpperCase();\n};\n"
  },
  {
    "path": "main/common/analytics.ts",
    "content": "'use strict';\n\nimport util from 'electron-util';\nimport {parse} from 'semver';\nimport {settings} from './settings';\n\n// TODO: Disabled because of https://github.com/wulkano/Kap/issues/1126\n/// const Insight = require('insight');\nconst pkg = require('../../package');\n\n/// const trackingCode = 'UA-84705099-2';\n/// const insight = new Insight({trackingCode, pkg});\n\nconst version = parse(pkg.version);\n\nexport const track = (...paths: string[]) => {\n  const allowAnalytics = settings.get('allowAnalytics');\n\n  if (allowAnalytics) {\n    console.log('Tracking', `v${version?.major}.${version?.minor}`, ...paths);\n    /// insight.track(`v${version?.major}.${version?.minor}`, ...paths);\n  }\n};\n\nexport const initializeAnalytics = () => {\n  if (util.isFirstAppLaunch()) {\n    /// insight.track('install');\n  }\n\n  if (settings.get('version') !== pkg.version) {\n    track('install');\n    settings.set('version', pkg.version);\n  }\n};\n"
  },
  {
    "path": "main/common/constants.ts",
    "content": "import {Format} from './types';\n\nexport const supportedVideoExtensions = ['mp4', 'mov', 'm4v'];\n\nconst formatExtensions = new Map([\n  ['av1', 'mp4'],\n  ['hevc', 'mp4']\n]);\n\nexport const formats = [Format.mp4, Format.hevc, Format.av1, Format.gif, Format.apng, Format.webm];\n\nexport const getFormatExtension = (format: Format) => formatExtensions.get(format) ?? format;\n\nexport const defaultInputDeviceId = 'SYSTEM_DEFAULT';\n"
  },
  {
    "path": "main/common/flags.ts",
    "content": "import Store from 'electron-store';\n\nexport const flags = new Store<{\n  backgroundEditorConversion: boolean;\n  editorDragTooltip: boolean;\n}>({\n  name: 'flags',\n  defaults: {\n    backgroundEditorConversion: false,\n    editorDragTooltip: false\n  }\n});\n"
  },
  {
    "path": "main/common/settings.ts",
    "content": "'use strict';\n\nimport {homedir} from 'os';\nimport Store from 'electron-store';\n\nconst {defaultInputDeviceId} = require('./constants');\nconst shortcutToAccelerator = require('../utils/shortcut-to-accelerator');\n\nexport const shortcuts = {\n  triggerCropper: 'Toggle Kap'\n};\n\nconst shortcutSchema = {\n  type: 'string',\n  default: ''\n};\n\ninterface Settings {\n  kapturesDir: string;\n  allowAnalytics: boolean;\n  showCursor: boolean;\n  highlightClicks: boolean;\n  record60fps: boolean;\n  loopExports: boolean;\n  recordKeyboardShortcut: boolean;\n  recordAudio: boolean;\n  audioInputDeviceId?: string;\n  cropperShortcut: {\n    metaKey: boolean;\n    altKey: boolean;\n    ctrlKey: boolean;\n    shiftKey: boolean;\n    character: string;\n  };\n  lossyCompression: boolean;\n  enableShortcuts: boolean;\n  shortcuts: {\n    [key in keyof typeof shortcuts]: string\n  };\n  version: string;\n}\n\nexport const settings = new Store<Settings>({\n  schema: {\n    kapturesDir: {\n      type: 'string',\n      default: `${homedir()}/Movies/Kaptures`\n    },\n    allowAnalytics: {\n      type: 'boolean',\n      default: true\n    },\n    showCursor: {\n      type: 'boolean',\n      default: true\n    },\n    highlightClicks: {\n      type: 'boolean',\n      default: false\n    },\n    record60fps: {\n      type: 'boolean',\n      default: false\n    },\n    loopExports: {\n      type: 'boolean',\n      default: true\n    },\n    recordKeyboardShortcut: {\n      type: 'boolean',\n      default: true\n    },\n    recordAudio: {\n      type: 'boolean',\n      default: false\n    },\n    audioInputDeviceId: {\n      type: [\n        'string',\n        'null'\n      ],\n      default: defaultInputDeviceId\n    },\n    cropperShortcut: {\n      type: 'object',\n      properties: {\n        metaKey: {\n          type: 'boolean',\n          default: true\n        },\n        altKey: {\n          type: 'boolean',\n          default: false\n        },\n        ctrlKey: {\n          type: 'boolean',\n          default: false\n        },\n        shiftKey: {\n          type: 'boolean',\n          default: true\n        },\n        character: {\n          type: 'string',\n          default: '5'\n        }\n      }\n    },\n    lossyCompression: {\n      type: 'boolean',\n      default: false\n    },\n    enableShortcuts: {\n      type: 'boolean',\n      default: true\n    },\n    shortcuts: {\n      type: 'object',\n      // eslint-disable-next-line unicorn/no-array-reduce\n      properties: Object.keys(shortcuts).reduce((acc, key) => ({...acc, [key]: shortcutSchema}), {}),\n      default: {}\n    },\n    version: {\n      type: 'string',\n      default: ''\n    }\n  }\n});\n\n// TODO: Remove this when we feel like everyone has migrated\nif (settings.has('recordKeyboardShortcut')) {\n  settings.set('enableShortcuts', settings.get('recordKeyboardShortcut'));\n  settings.delete('recordKeyboardShortcut');\n}\n\n// TODO: Remove this when we feel like everyone has migrated\nif (settings.has('cropperShortcut')) {\n  settings.set('shortcuts.triggerCropper', shortcutToAccelerator(settings.get('cropperShortcut')));\n  settings.delete('cropperShortcut');\n}\n\nsettings.set('cropper' as any, {});\nsettings.set('actionBar' as any, {});\n"
  },
  {
    "path": "main/common/system-permissions.ts",
    "content": "import {systemPreferences, shell, dialog, app} from 'electron';\nconst {hasScreenCapturePermission, hasPromptedForPermission} = require('mac-screen-capture-permissions');\nconst {ensureDockIsShowing} = require('../utils/dock');\n\nlet isDialogShowing = false;\n\nconst promptSystemPreferences = (options: {message: string; detail: string; systemPreferencesPath: string}) => async ({hasAsked}: {hasAsked?: boolean} = {}) => {\n  if (hasAsked || isDialogShowing) {\n    return false;\n  }\n\n  isDialogShowing = true;\n  await ensureDockIsShowing(async () => {\n    const {response} = await dialog.showMessageBox({\n      type: 'warning',\n      buttons: ['Open System Preferences', 'Cancel'],\n      defaultId: 0,\n      message: options.message,\n      detail: options.detail,\n      cancelId: 1\n    });\n    isDialogShowing = false;\n\n    if (response === 0) {\n      await openSystemPreferences(options.systemPreferencesPath);\n      app.quit();\n    }\n  });\n\n  return false;\n};\n\nexport const openSystemPreferences = async (path: string) => shell.openExternal(`x-apple.systempreferences:com.apple.preference.security?${path}`);\n\n// Microphone\n\nconst getMicrophoneAccess = () => systemPreferences.getMediaAccessStatus('microphone');\n\nconst microphoneFallback = promptSystemPreferences({\n  message: 'Kap cannot access the microphone.',\n  detail: 'Kap requires microphone access to be able to record audio. You can grant this in the System Preferences. Afterwards, launch Kap for the changes to take effect.',\n  systemPreferencesPath: 'Privacy_Microphone'\n});\n\nexport const ensureMicrophonePermissions = async (fallback = microphoneFallback) => {\n  const access = getMicrophoneAccess();\n\n  if (access === 'granted') {\n    return true;\n  }\n\n  if (access !== 'denied') {\n    const granted = await systemPreferences.askForMediaAccess('microphone');\n\n    if (granted) {\n      return true;\n    }\n\n    return fallback({hasAsked: true});\n  }\n\n  return fallback();\n};\n\nexport const hasMicrophoneAccess = () => getMicrophoneAccess() === 'granted';\n\n// Screen Capture (10.15 and newer)\n\nconst screenCaptureFallback = promptSystemPreferences({\n  message: 'Kap cannot record the screen.',\n  detail: 'Kap requires screen capture access to be able to record the screen. You can grant this in the System Preferences. Afterwards, launch Kap for the changes to take effect.',\n  systemPreferencesPath: 'Privacy_ScreenCapture'\n});\n\nexport const ensureScreenCapturePermissions = (fallback = screenCaptureFallback) => {\n  const hadAsked = hasPromptedForPermission();\n\n  const hasAccess = hasScreenCapturePermission();\n\n  if (hasAccess) {\n    return true;\n  }\n\n  fallback({hasAsked: !hadAsked});\n  return false;\n};\n\nexport const hasScreenCaptureAccess = () => hasScreenCapturePermission();\n\n"
  },
  {
    "path": "main/common/types/base.ts",
    "content": "import {Rectangle} from 'electron';\n\nexport enum Format {\n  gif = 'gif',\n  hevc = 'hevc',\n  mp4 = 'mp4',\n  webm = 'webm',\n  apng = 'apng',\n  av1 = 'av1'\n}\n\nexport enum Encoding {\n  h264 = 'h264',\n  hevc = 'hevc',\n  // eslint-disable-next-line unicorn/prevent-abbreviations\n  proRes422 = 'proRes422',\n  // eslint-disable-next-line unicorn/prevent-abbreviations\n  proRes4444 = 'proRes4444'\n}\n\nexport type App = {\n  url: string;\n  isDefault: boolean;\n  icon: string;\n  name: string;\n};\n\nexport interface ApertureOptions {\n  fps: number;\n  cropArea: Rectangle;\n  showCursor: boolean;\n  highlightClicks: boolean;\n  screenId: number;\n  audioDeviceId?: string;\n  videoCodec?: Encoding;\n}\n\nexport interface StartRecordingOptions {\n  cropperBounds: Rectangle;\n  screenBounds: Rectangle;\n  displayId: number;\n}\n"
  },
  {
    "path": "main/common/types/conversion-options.ts",
    "content": "import {App, Format} from './base';\n\nexport type CreateExportOptions = {\n  filePath: string;\n  conversionOptions: ConversionOptions;\n  format: Format;\n  plugins: {\n    share: {\n      pluginName: string;\n      serviceTitle: string;\n      app?: App;\n    };\n  };\n};\n\nexport type EditServiceInfo = {\n  pluginName: string;\n  serviceTitle: string;\n};\n\nexport type ConversionOptions = {\n  startTime: number;\n  endTime: number;\n  width: number;\n  height: number;\n  fps: number;\n  shouldCrop: boolean;\n  shouldMute: boolean;\n  editService?: EditServiceInfo;\n};\n\nexport enum ExportStatus {\n  inProgress = 'inProgress',\n  failed = 'failed',\n  canceled = 'canceled',\n  completed = 'completed'\n}\n"
  },
  {
    "path": "main/common/types/index.ts",
    "content": "export * from './base';\nexport * from './remote-states';\nexport * from './conversion-options';\nexport * from './window-states';\n"
  },
  {
    "path": "main/common/types/remote-states.ts",
    "content": "import {App, Format} from './base';\nimport {ExportStatus} from './conversion-options';\n\n// eslint-disable-next-line @typescript-eslint/ban-types\nexport type RemoteState<State = any, Actions extends Record<string, (...args: any[]) => any> = {}> = {\n  actions: Actions;\n  state: State;\n};\n\nexport type RemoteStateHook<Base extends RemoteState> = Base extends RemoteState<infer State, infer Actions> ? (\n  Actions & {\n    state: State;\n    isLoading: boolean;\n    refreshState: () => void;\n  }\n) : never;\n\nexport type RemoteStateHandler<Base extends RemoteState> = Base extends RemoteState<infer State, infer Actions> ? (sendUpdate: (state: State, id?: string) => void) => {\n  actions: {\n    [Key in keyof Actions]: Actions[Key] extends (...args: any[]) => any ? (id: string, ...args: Parameters<Actions[Key]>) => void : never\n  };\n  getState: (id: string) => State | undefined;\n} : never;\n\nexport interface ExportOptionsPlugin {\n  title: string;\n  pluginName: string;\n  pluginPath: string;\n  apps?: App[];\n  lastUsed: number;\n}\n\nexport type ExportOptionsFormat = {\n  plugins: ExportOptionsPlugin[];\n  format: Format;\n  prettyFormat: string;\n  lastUsed: number;\n};\n\nexport type ExportOptionsEditService = {\n  title: string;\n  pluginName: string;\n  pluginPath: string;\n  hasConfig: boolean;\n};\n\nexport type ExportOptions = {\n  formats: ExportOptionsFormat[];\n  editServices: ExportOptionsEditService[];\n  fpsHistory: {[key in Format]: number};\n};\n\nexport type EditorOptionsRemoteState = RemoteState<ExportOptions, {\n  updatePluginUsage: ({format, plugin}: {\n    format: Format;\n    plugin: string;\n  }) => void;\n  updateFpsUsage: ({format, fps}: {\n    format: Format;\n    fps: number;\n  }) => void;\n}>;\n\nexport interface ExportState {\n  id: string;\n  title: string;\n  description: string;\n  message: string;\n  progress?: number;\n  image?: string;\n  filePath?: string;\n  error?: Error;\n  fileSize?: string;\n  status: ExportStatus;\n  canCopy: boolean;\n  disableOutputActions: boolean;\n  canPreviewExport: boolean;\n  titleWithFormat: string;\n}\n\nexport type ExportsRemoteState = RemoteState<ExportState, {\n  copy: () => void;\n  cancel: () => void;\n  retry: () => void;\n  openInEditor: () => void;\n  showInFolder: () => void;\n}>;\n\nexport type ExportsListRemoteState = RemoteState<string[]>;\n"
  },
  {
    "path": "main/common/types/window-states.ts",
    "content": "\nexport interface EditorWindowState {\n  fps: number;\n  previewFilePath: string;\n  filePath: string;\n  title: string;\n  conversionId?: string;\n}\n"
  },
  {
    "path": "main/conversion.ts",
    "content": "import fs from 'fs';\nimport {app, clipboard} from 'electron';\nimport {EventEmitter} from 'events';\nimport {ConversionOptions, Format} from './common/types';\nimport {Video} from './video';\nimport {convertTo} from './converters';\nimport hash from 'object-hash';\nimport {notify} from './utils/notifications';\nimport PCancelable from 'p-cancelable';\nimport prettyBytes from 'pretty-bytes';\nimport TypedEventEmitter from 'typed-emitter';\n\nconst plist = require('plist');\n\n// A conversion object describes the process of converting a video or recording\n// using ffmpeg that can then be shared multiple times using Share plugins\nexport default class Conversion extends (EventEmitter as new () => TypedEventEmitter<ConversionEvents>) {\n  static conversionMap = new Map<string, Conversion>();\n\n  static get all() {\n    return [...this.conversionMap.values()];\n  }\n\n  readonly id: string;\n  finalSize?: string;\n  convertedFilePath?: string;\n\n  get canCopy() {\n    return Boolean(this.convertedFilePath && [Format.gif, Format.apng, Format.mp4].includes(this.format));\n  }\n\n  private conversionProcess?: PCancelable<string>;\n\n  constructor(\n    public readonly video: Video,\n    public readonly format: Format,\n    public readonly options: ConversionOptions\n  ) {\n    // eslint-disable-next-line constructor-super\n    super();\n\n    this.id = hash({\n      filePath: video.filePath,\n      format,\n      options\n    });\n\n    Conversion.conversionMap.set(this.id, this);\n  }\n\n  static fromId(id: string) {\n    return this.conversionMap.get(id);\n  }\n\n  static getOrCreate(video: Video, format: Format, options: ConversionOptions) {\n    const id = hash({\n      filePath: video.filePath,\n      format,\n      options\n    });\n\n    return this.fromId(id) ?? new Conversion(video, format, options);\n  }\n\n  copy = () => {\n    clipboard.writeBuffer('NSFilenamesPboardType', Buffer.from(plist.build([this.convertedFilePath])));\n    notify({\n      body: 'The file has been copied to the clipboard',\n      title: app.name\n    });\n  };\n\n  async filePathExists() {\n    if (!this.convertedFilePath) {\n      return false;\n    }\n\n    try {\n      await fs.promises.access(this.convertedFilePath, fs.constants.F_OK);\n      return true;\n    } catch {\n      return false;\n    }\n  }\n\n  filePath = async () => {\n    if (!this.conversionProcess) {\n      this.start();\n    }\n\n    try {\n      this.convertedFilePath = await this.conversionProcess;\n      this.emit('completed');\n      this.calculateFileSize(this.convertedFilePath);\n      return this.convertedFilePath!;\n    } catch (error) {\n      // Ensure we re-try the conversion if it fails\n      this.conversionProcess = undefined;\n      if (!(error as any)?.isCanceled) {\n        this.emit('error', error as any);\n      }\n\n      throw error;\n    }\n  };\n\n  cancel = () => {\n    if (!this.conversionProcess?.isCanceled && !this.convertedFilePath) {\n      this.conversionProcess?.cancel();\n    }\n  };\n\n  private readonly onConversionProgress = (action: string, progress: number, estimate?: string) => {\n    const text = estimate ? `${action} — ${estimate} remaining` : `${action}…`;\n    this.emit('progress', text, Math.max(Math.min(progress, 1), 0));\n  };\n\n  private readonly calculateFileSize = async (filePath?: string) => {\n    if (!filePath) {\n      return;\n    }\n\n    try {\n      const {size} = await fs.promises.stat(filePath);\n      this.finalSize = prettyBytes(size);\n      this.emit('file-size', this.finalSize);\n    } catch {}\n  };\n\n  private readonly start = () => {\n    this.conversionProcess = convertTo(\n      this.format,\n      {\n        ...this.options,\n        defaultFileName: this.video.title,\n        inputPath: this.video.filePath,\n        onProgress: this.onConversionProgress,\n        onCancel: () => {\n          this.emit('cancel');\n        }\n      },\n      this.video.encoding\n    );\n  };\n}\n\ninterface ConversionEvents {\n  progress: (text: string, percentage: number) => void;\n  error: (error: Error) => void;\n  cancel: () => void;\n  completed: () => void;\n  'file-size': (size: string) => void;\n}\n"
  },
  {
    "path": "main/converters/h264.ts",
    "content": "import PCancelable from 'p-cancelable';\nimport tempy from 'tempy';\nimport {compress, convert} from './process';\nimport {areDimensionsEven, conditionalArgs, ConvertOptions, makeEven} from './utils';\nimport {settings} from '../common/settings';\nimport os from 'os';\nimport {Format} from '../common/types';\nimport fs from 'fs';\n\n// `time ffmpeg -i original.mp4 -vf fps=30,scale=480:-1::flags=lanczos,palettegen palette.png`\n// `time ffmpeg -i original.mp4 -i palette.png -filter_complex 'fps=30,scale=-1:-1:flags=lanczos[x]; [x][1:v]paletteuse' palette.gif`\nconst convertToGif = PCancelable.fn(async (options: ConvertOptions, onCancel: PCancelable.OnCancelFunction) => {\n  const palettePath = tempy.file({extension: 'png'});\n\n  const paletteProcess = convert(palettePath, {shouldTrack: false}, conditionalArgs(\n    '-i', options.inputPath,\n    '-vf', `fps=${options.fps}${options.shouldCrop ? `,scale=${options.width}:${options.height}:flags=lanczos` : ''},palettegen`,\n    {\n      args: [\n        '-ss',\n        options.startTime.toString(),\n        '-to',\n        options.endTime.toString()\n      ],\n      if: options.shouldCrop\n    },\n    palettePath\n  ));\n\n  onCancel(() => {\n    paletteProcess.cancel();\n  });\n\n  await paletteProcess;\n\n  // Sometimes if the clip is too short or fps too low, the palette is not generated\n  const hasPalette = fs.existsSync(palettePath);\n\n  const shouldLoop = settings.get('loopExports');\n\n  const conversionProcess = convert(options.outputPath, {\n    onProgress: (progress, estimate) => {\n      options.onProgress('Converting', progress, estimate);\n    },\n    startTime: options.startTime,\n    endTime: options.endTime\n  }, conditionalArgs(\n    '-i', options.inputPath,\n    {\n      args: [\n        '-i',\n        palettePath,\n        '-filter_complex',\n        `fps=${options.fps}${options.shouldCrop ? `,scale=${options.width}:${options.height}:flags=lanczos` : ''}[x]; [x][1:v]paletteuse`\n      ],\n      if: hasPalette\n    },\n    {\n      args: [\n        '-vf',\n        `fps=${options.fps}${options.shouldCrop ? `,scale=${options.width}:${options.height}:flags=lanczos` : ''}`\n      ],\n      if: !hasPalette\n    },\n    '-loop', shouldLoop ? '0' : '-1', // 0 == forever; -1 == no loop\n    {\n      args: [\n        '-ss',\n        options.startTime.toString(),\n        '-to',\n        options.endTime.toString()\n      ],\n      if: options.shouldCrop\n    },\n    options.outputPath\n  ));\n\n  onCancel(() => {\n    conversionProcess.cancel();\n  });\n\n  await conversionProcess;\n\n  const compressProcess = compress(options.outputPath, {\n    onProgress: (progress, estimate) => {\n      options.onProgress('Compressing', progress, estimate);\n    },\n    startTime: options.startTime,\n    endTime: options.endTime\n  }, [\n    '--batch',\n    options.outputPath\n  ]);\n\n  onCancel(() => {\n    compressProcess.cancel();\n  });\n\n  await compressProcess;\n\n  return options.outputPath;\n});\n\n// eslint-disable-next-line @typescript-eslint/promise-function-async\nconst convertToMp4 = (options: ConvertOptions) => convert(options.outputPath, {\n  onProgress: (progress, estimate) => {\n    options.onProgress('Converting', progress, estimate);\n  },\n  startTime: options.startTime,\n  endTime: options.endTime\n}, conditionalArgs(\n  '-i', options.inputPath,\n  '-r', options.fps.toString(),\n  {\n    args: ['-an'],\n    if: options.shouldMute\n  },\n  {\n    args: [\n      '-s',\n      `${makeEven(options.width)}x${makeEven(options.height)}`,\n      '-ss',\n      options.startTime.toString(),\n      '-to',\n      options.endTime.toString()\n    ],\n    if: options.shouldCrop || !areDimensionsEven(options)\n  },\n  options.outputPath\n));\n\n// eslint-disable-next-line @typescript-eslint/promise-function-async\nconst convertToWebm = (options: ConvertOptions) => convert(options.outputPath, {\n  onProgress: (progress, estimate) => {\n    options.onProgress('Converting', progress, estimate);\n  },\n  startTime: options.startTime,\n  endTime: options.endTime\n}, conditionalArgs(\n  '-i', options.inputPath,\n  // http://wiki.webmproject.org/ffmpeg\n  // https://trac.ffmpeg.org/wiki/Encode/VP9\n  '-threads', Math.max(os.cpus().length - 1, 1).toString(),\n  '-deadline', 'good', // `best` is twice as slow and only slighty better\n  '-b:v', '1M', // Bitrate (same as the MP4)\n  '-codec:v', 'vp9',\n  '-codec:a', 'vorbis',\n  '-ac', '2', // https://stackoverflow.com/questions/19004762/ffmpeg-covert-from-mp4-to-webm-only-working-on-some-files\n  '-strict', '-2', // Needed because `vorbis` is experimental\n  '-r', options.fps.toString(),\n  {\n    args: ['-an'],\n    if: options.shouldMute\n  },\n  {\n    args: [\n      '-s',\n      `${makeEven(options.width)}x${makeEven(options.height)}`,\n      '-ss',\n      options.startTime.toString(),\n      '-to',\n      options.endTime.toString()\n    ],\n    if: options.shouldCrop || !areDimensionsEven(options)\n  },\n  options.outputPath\n));\n\n// eslint-disable-next-line @typescript-eslint/promise-function-async\nconst convertToAv1 = (options: ConvertOptions) => convert(options.outputPath, {\n  onProgress: (progress, estimate) => {\n    options.onProgress('Converting', progress, estimate);\n  },\n  startTime: options.startTime,\n  endTime: options.endTime\n}, conditionalArgs(\n  '-i', options.inputPath,\n  '-r', options.fps.toString(),\n  '-c:v', 'libaom-av1',\n  '-c:a', 'libopus',\n  '-crf', '34',\n  '-b:v', '0',\n  '-strict', 'experimental',\n  // Enables row-based multi-threading which maximizes CPU usage\n  // https://trac.ffmpeg.org/wiki/Encode/AV1\n  '-cpu-used', '4',\n  '-row-mt', '1',\n  '-tiles', '2x2',\n  {\n    args: ['-an'],\n    if: options.shouldMute\n  },\n  {\n    args: [\n      '-s',\n      `${makeEven(options.width)}x${makeEven(options.height)}`,\n      '-ss',\n      options.startTime.toString(),\n      '-to',\n      options.endTime.toString()\n    ],\n    if: options.shouldCrop || !areDimensionsEven(options)\n  },\n  options.outputPath\n));\n\n// eslint-disable-next-line @typescript-eslint/promise-function-async\nconst convertToHevc = (options: ConvertOptions) => convert(options.outputPath, {\n  onProgress: (progress, estimate) => {\n    options.onProgress('Converting', progress, estimate);\n  },\n  startTime: options.startTime,\n  endTime: options.endTime\n}, conditionalArgs(\n  '-i', options.inputPath,\n  '-r', options.fps.toString(),\n  '-c:v', 'libx265',\n  '-c:a', 'libopus',\n  '-preset', 'medium',\n  '-tag:v', 'hvc1', // Metadata for macOS\n  {\n    args: ['-an'],\n    if: options.shouldMute\n  },\n  {\n    args: [\n      '-s',\n      `${makeEven(options.width)}x${makeEven(options.height)}`,\n      '-ss',\n      options.startTime.toString(),\n      '-to',\n      options.endTime.toString()\n    ],\n    if: options.shouldCrop || !areDimensionsEven(options)\n  },\n  options.outputPath\n));\n\n// eslint-disable-next-line @typescript-eslint/promise-function-async\nconst convertToApng = (options: ConvertOptions) => convert(options.outputPath, {\n  onProgress: (progress, estimate) => {\n    options.onProgress('Converting', progress, estimate);\n  },\n  startTime: options.startTime,\n  endTime: options.endTime\n}, conditionalArgs(\n  '-i', options.inputPath,\n  '-vf', `fps=${options.fps}${options.shouldCrop ? `,scale=${options.width}:${options.height}:flags=lanczos` : ''}`,\n  // Strange for APNG instead of -loop it uses -plays see: https://stackoverflow.com/questions/43795518/using-ffmpeg-to-create-looping-apng\n  '-plays', settings.get('loopExports') ? '0' : '1', // 0 == forever; 1 == no loop\n  {\n    args: ['-an'],\n    if: options.shouldMute\n  },\n  {\n    args: [\n      '-ss',\n      options.startTime.toString(),\n      '-to',\n      options.endTime.toString()\n    ],\n    if: options.shouldCrop\n  },\n  options.outputPath\n));\n\n// eslint-disable-next-line @typescript-eslint/promise-function-async\nexport const crop = (options: ConvertOptions) => convert(options.outputPath, {\n  onProgress: (progress, estimate) => {\n    options.onProgress('Cropping', progress, estimate);\n  },\n  startTime: options.startTime,\n  endTime: options.endTime\n}, conditionalArgs(\n  '-i', options.inputPath,\n  '-s', `${makeEven(options.width)}x${makeEven(options.height)}`,\n  '-ss', options.startTime.toString(),\n  '-to', options.endTime.toString(),\n  options.outputPath\n));\n\nexport default new Map([\n  [Format.gif, convertToGif],\n  [Format.mp4, convertToMp4],\n  [Format.hevc, convertToHevc],\n  [Format.webm, convertToWebm],\n  [Format.apng, convertToApng],\n  [Format.av1, convertToAv1]\n]);\n"
  },
  {
    "path": "main/converters/index.ts",
    "content": "import path from 'path';\nimport tempy from 'tempy';\nimport {Encoding, Format} from '../common/types';\nimport {track} from '../common/analytics';\nimport h264Converters, {crop as h264Crop} from './h264';\nimport {ConvertOptions} from './utils';\nimport {getFormatExtension} from '../common/constants';\nimport PCancelable, {OnCancelFunction} from 'p-cancelable';\nimport {convert} from './process';\nimport {plugins} from '../plugins';\nimport {EditServiceContext} from '../plugins/service-context';\nimport {settings} from '../common/settings';\nimport {Except} from 'type-fest';\n\nconst converters = new Map([\n  [Encoding.h264, h264Converters]\n]);\n\nconst croppingHandlers = new Map([\n  [Encoding.h264, h264Crop]\n]);\n\n// eslint-disable-next-line @typescript-eslint/promise-function-async\nexport const convertTo = (\n  format: Format,\n  options: Except<ConvertOptions, 'outputPath'> & {defaultFileName: string},\n  encoding: Encoding = Encoding.h264\n) => {\n  if (!converters.has(encoding)) {\n    throw new Error(`Unsupported encoding: ${encoding}`);\n  }\n\n  const converter = converters.get(encoding)?.get(format);\n\n  if (!converter) {\n    throw new Error(`Unsupported file format for ${encoding}: ${format}`);\n  }\n\n  track(`file/export/encoding/${encoding}`);\n  track(`file/export/format/${format}`);\n\n  const conversionOptions = {\n    outputPath: path.join(tempy.directory(), `${options.defaultFileName}.${getFormatExtension(format)}`),\n    ...options\n  };\n\n  if (options.editService) {\n    const croppingHandler = croppingHandlers.get(encoding);\n\n    if (!croppingHandler) {\n      throw new Error(`Unsupported encoding: ${encoding}`);\n    }\n\n    return convertWithEditPlugin({...conversionOptions, format, croppingHandler, converter});\n  }\n\n  return converter(conversionOptions);\n};\n\nconst convertWithEditPlugin = PCancelable.fn(\n  async (\n    options: ConvertOptions & {\n      format: Format;\n      converter: (options: ConvertOptions) => PCancelable<string>;\n      croppingHandler: (options: ConvertOptions) => PCancelable<string>;\n    },\n    onCancel: OnCancelFunction\n  ) => {\n    let croppedPath: string;\n    let isCanceled = false;\n\n    if (options.shouldCrop) {\n      croppedPath = tempy.file({extension: path.extname(options.inputPath)});\n\n      options.onProgress('Cropping', 0);\n\n      const cropProcess = options.croppingHandler({\n        ...options,\n        outputPath: croppedPath\n      });\n\n      onCancel(() => {\n        isCanceled = true;\n        cropProcess.cancel();\n      });\n\n      await cropProcess;\n\n      if (isCanceled) {\n        return '';\n      }\n    } else {\n      croppedPath = options.inputPath;\n    }\n\n    // eslint-disable-next-line @typescript-eslint/promise-function-async\n    const convertFunction = (args: string[], text = 'Converting') => new PCancelable<void>(async (resolve, reject, onCancel) => {\n      try {\n        const process = convert(\n          '', {\n            shouldTrack: false,\n            startTime: options.startTime,\n            endTime: options.endTime,\n            onProgress: (progress, estimate) => {\n              options.onProgress(text, progress, estimate);\n            }\n          }, args\n        );\n\n        onCancel(() => {\n          process.cancel();\n        });\n        await process;\n        resolve();\n      } catch (error) {\n        reject(error);\n      }\n    });\n\n    const editPath = tempy.file({extension: path.extname(croppedPath)});\n\n    const editPlugin = plugins.editPlugins.find(plugin => {\n      return plugin.name === options.editService?.pluginName;\n    });\n\n    const editService = editPlugin?.editServices.find(service => {\n      return service.title === options.editService?.serviceTitle;\n    });\n\n    if (!editService || !editPlugin) {\n      throw new Error(`Edit service ${options.editService?.serviceTitle} not found`);\n    }\n\n    const editProcess = editService.action(\n      new EditServiceContext({\n        plugin: editPlugin,\n        onCancel: options.onCancel,\n        onProgress: options.onProgress,\n        convert: convertFunction,\n        inputPath: croppedPath,\n        outputPath: editPath,\n        exportOptions: {\n          width: options.width,\n          height: options.height,\n          format: options.format,\n          fps: options.fps,\n          duration: options.endTime - options.startTime,\n          isMuted: options.shouldMute,\n          loop: settings.get('loopExports')\n        }\n      })\n    );\n\n    onCancel(() => {\n      isCanceled = true;\n      // @ts-expect-error\n      if (editProcess.cancel && typeof editProcess.cancel === 'function') {\n        (editProcess as PCancelable<void>).cancel();\n      }\n    });\n\n    await editProcess;\n\n    if (isCanceled) {\n      return '';\n    }\n\n    track(`plugins/used/edit/${options.editService?.pluginName}`);\n\n    const conversionProcess = options.converter({\n      ...options,\n      shouldCrop: false,\n      inputPath: editPath\n    });\n\n    onCancel(() => {\n      conversionProcess.cancel();\n    });\n\n    return conversionProcess;\n  }\n);\n"
  },
  {
    "path": "main/converters/process.ts",
    "content": "import util from 'electron-util';\nimport execa from 'execa';\nimport moment from 'moment';\nimport PCancelable from 'p-cancelable';\nimport tempy from 'tempy';\nimport path from 'path';\n\nimport {track} from '../common/analytics';\nimport {conditionalArgs, extractProgressFromStderr} from './utils';\nimport {settings} from '../common/settings';\n\nimport ffmpegPath from '../utils/ffmpeg-path';\n\nconst gifsicle = require('gifsicle');\nconst gifsiclePath = util.fixPathForAsarUnpack(gifsicle);\n\nenum Mode {\n  convert,\n  compress\n}\n\nconst modes = new Map([\n  [Mode.convert, ffmpegPath],\n  [Mode.compress, gifsiclePath]\n]);\n\nexport interface ProcessOptions {\n  shouldTrack?: boolean;\n  startTime?: number;\n  endTime?: number;\n  onProgress?: (progress: number, estimate?: string) => void;\n}\n\nconst defaultProcessOptions = {\n  shouldTrack: true\n};\n\nconst createProcess = (mode: Mode) => {\n  const program = modes.get(mode)!;\n\n  // eslint-disable-next-line @typescript-eslint/promise-function-async\n  return (outputPath: string, options: ProcessOptions, args: string[]) => {\n    const {\n      shouldTrack,\n      startTime = 0,\n      endTime = 0,\n      onProgress\n    } = {\n      ...defaultProcessOptions,\n      ...options\n    };\n\n    const modeName = Mode[mode];\n    const trackConversionEvent = (eventName: string) => {\n      if (shouldTrack) {\n        track(`file/export/${modeName}/${eventName}`);\n      }\n    };\n\n    return new PCancelable<string>((resolve, reject, onCancel) => {\n      const runner = execa(program, args);\n      const conversionStartTime = Date.now();\n\n      onCancel(() => {\n        trackConversionEvent('canceled');\n        runner.kill();\n      });\n\n      const durationMs = moment.duration(endTime - startTime, 'seconds').asMilliseconds();\n\n      let stderr = '';\n      runner.stderr?.setEncoding('utf8');\n      runner.stderr?.on('data', data => {\n        stderr += data;\n\n        const progressData = extractProgressFromStderr(data, conversionStartTime, durationMs);\n\n        if (progressData) {\n          onProgress?.(progressData.progress, progressData.estimate);\n        }\n      });\n\n      const failWithError = (reason: unknown) => {\n        trackConversionEvent('failed');\n        reject(reason);\n      };\n\n      runner.on('error', failWithError);\n\n      runner.on('exit', code => {\n        if (code === 0) {\n          trackConversionEvent('completed');\n          resolve(outputPath);\n        } else {\n          failWithError(new Error(`${program} exited with code: ${code ?? 0}\\n\\n${stderr}`));\n        }\n      });\n\n      runner.catch(failWithError);\n    });\n  };\n};\n\nexport const convert = createProcess(Mode.convert);\nconst compressFunction = createProcess(Mode.compress);\n\n// eslint-disable-next-line @typescript-eslint/promise-function-async\nexport const compress = (outputPath: string, options: ProcessOptions, args: string[]) => {\n  const useLossy = settings.get('lossyCompression', false);\n\n  return compressFunction(\n    outputPath,\n    options,\n    conditionalArgs(args, {args: ['--lossy=50'], if: useLossy})\n  );\n};\n\nexport const mute = PCancelable.fn(async (inputPath: string, onCancel: PCancelable.OnCancelFunction) => {\n  const mutedPath = tempy.file({extension: path.extname(inputPath)});\n\n  const converter = convert(mutedPath, {shouldTrack: false}, [\n    '-i',\n    inputPath,\n    '-an',\n    '-vcodec',\n    'copy',\n    mutedPath\n  ]);\n\n  onCancel(() => {\n    converter.cancel();\n  });\n\n  return converter;\n});\n"
  },
  {
    "path": "main/converters/utils.ts",
    "content": "import moment from 'moment';\nimport prettyMilliseconds from 'pretty-ms';\n\nexport interface ConvertOptions {\n  inputPath: string;\n  outputPath: string;\n  shouldCrop: boolean;\n  startTime: number;\n  endTime: number;\n  width: number;\n  height: number;\n  fps: number;\n  shouldMute: boolean;\n  onCancel: () => void;\n  onProgress: (action: string, progress: number, estimate?: string) => void;\n  editService?: {\n    pluginName: string;\n    serviceTitle: string;\n  };\n}\n\nexport const makeEven = (number: number) => 2 * Math.round(number / 2);\n\nexport const areDimensionsEven = ({width, height}: {width: number; height: number}) => width % 2 === 0 && height % 2 === 0;\n\nexport const extractProgressFromStderr = (stderr: string, conversionStartTime: number, durationMs: number) => {\n  const conversionDuration = Date.now() - conversionStartTime;\n  const data = stderr.trim();\n\n  const speed = Number.parseFloat(/speed=\\s*(-?\\d+(,\\d+)*(\\.\\d+(e\\d+)?)?)/gm.exec(data)?.[1] ?? '0');\n  const processedMs = moment.duration(/time=\\s*(\\d\\d:\\d\\d:\\d\\d.\\d\\d)/gm.exec(data)?.[1] ?? 0).asMilliseconds();\n\n  if (speed > 0) {\n    const progress = processedMs / durationMs;\n\n    // Wait 2 seconds in the conversion for speed to be stable\n    // Either 2 seconds of the video or 15 seconds real time (for super slow conversion like AV1)\n    if (processedMs > 2 * 1000 || conversionDuration > 15 * 1000) {\n      const msRemaining = (durationMs - processedMs) / speed;\n\n      return {\n        progress,\n        estimate: prettyMilliseconds(Math.max(msRemaining, 1000), {compact: true})\n      };\n    }\n\n    return {progress};\n  }\n\n  return undefined;\n};\n\ntype ArgType = string[] | string | {args: string[]; if: boolean};\n\n// Resolve conditional args\n//\n// conditionalArgs(['default', 'args'], {args: ['ignore', 'these'], if: false});\n// => ['default', 'args']\nexport const conditionalArgs = (...args: ArgType[]): string[] => {\n  return args.flatMap(arg => {\n    if (typeof arg === 'string') {\n      return [arg];\n    }\n\n    if (Array.isArray(arg)) {\n      return arg;\n    }\n\n    return arg.if ? arg.args : [];\n  });\n};\n"
  },
  {
    "path": "main/export.ts",
    "content": "import {ipcMain, dialog, app} from 'electron';\nimport {EventEmitter} from 'events';\nimport PCancelable, {CancelError, OnCancelFunction} from 'p-cancelable';\nimport Conversion from './conversion';\nimport {InstalledPlugin} from './plugins/plugin';\nimport {ShareService} from './plugins/service';\nimport {ShareServiceContext} from './plugins/service-context';\nimport {prettifyFormat} from './utils/formats';\nimport {ipcMain as ipc} from 'electron-better-ipc';\nimport {setExportMenuItemState} from './menus/utils';\nimport {Video} from './video';\nimport {ConversionOptions, ExportState, ExportStatus, Format, CreateExportOptions} from './common/types';\nimport {showError} from './utils/errors';\nimport TypedEventEmitter from 'typed-emitter';\nimport {plugins} from './plugins';\nimport {askForTargetFilePath} from './plugins/built-in/save-file-plugin';\nimport path from 'path';\nimport {ensureDockIsShowingSync} from './utils/dock';\nimport {windowManager} from './windows/manager';\n\nexport interface ExportOptions {\n  plugin: InstalledPlugin;\n  service: ShareService;\n  extras: Record<string, unknown>;\n}\n\nexport default class Export extends (EventEmitter as new () => TypedEventEmitter<ExportEvents>) {\n  static exportsMap = new Map<string, Export>();\n  static events = new EventEmitter() as TypedEventEmitter<ExportsEvents>;\n\n  static get all() {\n    return [...this.exportsMap.values()];\n  }\n\n  readonly createdAt: number = Date.now();\n  conversion?: Conversion;\n  status: ExportStatus = ExportStatus.inProgress;\n\n  private text = 'Loading…';\n  private percentage = 0;\n\n  private readonly context: ShareServiceContext;\n  private process?: PCancelable<void>;\n  private areOutputActionsDisabled = false;\n  private error?: Error;\n  private readonly description: string;\n\n  private readonly _start = PCancelable.fn(async (onCancel: OnCancelFunction) => {\n    this.error = undefined;\n    this.text = 'Loading…';\n    const action = this.options.service.action(this.context) as any;\n\n    onCancel(() => {\n      if (action.cancel && typeof action.cancel === 'function') {\n        action.cancel();\n      }\n\n      this.context.isCanceled = true;\n    });\n\n    try {\n      await action;\n      this.status = ExportStatus.completed;\n      this.text = 'Export completed';\n      this.emit('updated', this.data);\n    } catch (error) {\n      this.captureError(error as any);\n    }\n  });\n\n  constructor(\n    public readonly video: Video,\n    private readonly format: Format,\n    private readonly conversionOptions: ConversionOptions,\n    private readonly options: ExportOptions,\n    private readonly title: string = video.title\n  ) {\n    // eslint-disable-next-line constructor-super\n    super();\n    Export.addExport(this);\n    video.generatePreviewImage();\n\n    this.description = `${this.conversionOptions.width} x ${this.conversionOptions.height} at ${this.conversionOptions.fps} FPS`;\n\n    this.context = new ShareServiceContext({\n      plugin: options.plugin,\n      format,\n      prettyFormat: prettifyFormat(format),\n      defaultFileName: video.title,\n      filePath: this.filePath,\n      onProgress: this.onProgress,\n      onCancel: this.cancel\n    });\n\n    // Used for built-in plugins like save-to-disk\n    for (const [key, value] of Object.entries(options.extras)) {\n      (this.context as any)[key] = value;\n    }\n\n    setExportMenuItemState(true);\n  }\n\n  static addExport = (newExport: Export) => {\n    Export.exportsMap.set(newExport.id, newExport);\n    Export.events.emit('added', newExport.data);\n\n    newExport.on('updated', state => Export.events.emit('updated', state));\n  };\n\n  static fromId(id: string) {\n    return this.exportsMap.get(id);\n  }\n\n  get id() {\n    return this.createdAt.toString();\n  }\n\n  get canPreviewExport() {\n    return [Format.gif, Format.apng].includes(this.format) && this.finalFilePath !== undefined;\n  }\n\n  get finalFilePath() {\n    const filePath = this.conversion?.convertedFilePath;\n\n    // If Save To Disk plugin is used, open the file in the final destination, not the temp one\n    return filePath && ((this.options.extras.targetFilePath as string) ?? filePath);\n  }\n\n  get data(): ExportState {\n    return {\n      title: this.title,\n      titleWithFormat: `${this.title}.${this.format}`,\n      description: this.description,\n      canCopy: this.conversion?.canCopy ?? false,\n      status: this.status,\n      message: this.text,\n      progress: this.percentage ?? 0,\n      image: this.video.previewImage?.data,\n      id: this.id,\n      filePath: this.conversion?.convertedFilePath,\n      error: this.error,\n      fileSize: this.conversion?.finalSize,\n      disableOutputActions: this.areOutputActionsDisabled,\n      canPreviewExport: this.canPreviewExport\n    };\n  }\n\n  filePath = async ({fileType}: {fileType?: Format} = {}) => {\n    if (fileType) {\n      this.areOutputActionsDisabled = true;\n    }\n\n    const format = fileType ?? this.format;\n\n    this.conversion = Conversion.getOrCreate(this.video, format, this.conversionOptions);\n    this.setupConversionListeners();\n\n    return this.conversion.filePath();\n  };\n\n  start = async () => {\n    try {\n      this.process = this._start();\n      await this.process;\n    } catch (error) {\n      this.captureError(error as any);\n    }\n  };\n\n  onProgress = (text: string, percentage: number) => {\n    if (this.status !== ExportStatus.inProgress) {\n      return;\n    }\n\n    this.text = text;\n    this.percentage = percentage;\n    this.emit('updated', this.data);\n  };\n\n  cancel = () => {\n    this.process?.cancel();\n    this.conversion?.cancel();\n    this.status = ExportStatus.canceled;\n    this.text = 'Export canceled';\n    this.context.isCanceled = true;\n    this.emit('updated', this.data);\n  };\n\n  retry = () => {\n    this.status = ExportStatus.inProgress;\n    this.error = undefined;\n    this.text = '';\n    this.start();\n    this.emit('updated', this.data);\n  };\n\n  private readonly captureError = (error: Error, fromConversion = false) => {\n    if ((error as CancelError).isCanceled) {\n      this.text = 'Export canceled';\n      this.status = ExportStatus.canceled;\n    } else {\n      this.text = 'Export failed';\n      this.status = ExportStatus.failed;\n\n      if (!this.error) {\n        this.error = error;\n        showError(error, fromConversion ? undefined : {plugin: this.options.plugin});\n      }\n    }\n\n    this.emit('updated', this.data);\n  };\n\n  private readonly captureConversionError = (error: Error) => this.captureError(error, true);\n\n  private readonly setupConversionListeners = () => {\n    this.conversion?.once('file-size', () => this.emit('updated', this.data));\n\n    this.conversion?.on('cancel', this.cancel);\n    this.conversion?.on('progress', this.onProgress);\n    this.conversion?.on('error', this.captureConversionError);\n    this.conversion?.on('completed', this.cleanConversionListeners);\n  };\n\n  private readonly cleanConversionListeners = () => {\n    this.conversion?.removeListener('cancel', this.cancel);\n    this.conversion?.removeListener('progress', this.onProgress);\n    this.conversion?.removeListener('error', this.captureConversionError);\n  };\n}\n\ninterface ExportEvents {\n  updated: (state: ExportState) => void;\n}\n\ninterface ExportsEvents {\n  added: (state: ExportState) => void;\n  updated: (state: ExportState) => void;\n}\n\nexport const setUpExportsListeners = () => {\n  ipcMain.on('drag-export', async (event: any, id: string) => {\n    const exportMap = Export.exportsMap.get(id);\n    const conversion = exportMap?.conversion;\n\n    if (conversion && (await conversion.filePathExists()) && exportMap?.status === ExportStatus.completed) {\n      event.sender.startDrag({\n        file: exportMap?.finalFilePath ?? conversion.convertedFilePath,\n        icon: await conversion.video.getDragIcon(conversion.options)\n      });\n    }\n  });\n\n  ipc.answerRenderer('create-export', async ({\n    filePath, conversionOptions, format, plugins: pluginOptions\n  }: CreateExportOptions, window) => {\n    const video = Video.fromId(filePath);\n    const extras: Record<string, any> = {\n      appUrl: pluginOptions.share.app?.url\n    };\n\n    if (!video) {\n      return;\n    }\n\n    if (pluginOptions.share.pluginName === '_saveToDisk') {\n      const targetFilePath = await askForTargetFilePath(\n        window,\n        format,\n        video.title\n      );\n\n      if (targetFilePath) {\n        extras.targetFilePath = targetFilePath;\n      } else {\n        return;\n      }\n    }\n\n    const exportPlugin = plugins.sharePlugins.find(plugin => {\n      return plugin.name === pluginOptions.share.pluginName;\n    });\n\n    const exportService = exportPlugin?.shareServices.find(service => {\n      return service.title === pluginOptions.share.serviceTitle;\n    });\n\n    if (!exportPlugin || !exportService) {\n      return;\n    }\n\n    const newExport = new Export(\n      video,\n      format,\n      conversionOptions,\n      {\n        plugin: exportPlugin,\n        service: exportService,\n        extras\n      },\n      extras.targetFilePath && path.parse(extras.targetFilePath).name\n    );\n\n    newExport.start();\n\n    return newExport.id;\n  });\n\n  app.on('before-quit', event => {\n    if (Export.all.some(exp => exp.status === ExportStatus.inProgress)) {\n      windowManager.exports?.open();\n\n      ensureDockIsShowingSync(() => {\n        const buttonIndex = dialog.showMessageBoxSync({\n          type: 'question',\n          buttons: [\n            'Continue',\n            'Quit'\n          ],\n          defaultId: 0,\n          cancelId: 1,\n          message: 'Do you want to continue exporting?',\n          detail: 'Kap is currently exporting files. If you quit, the export task will be canceled.'\n        });\n\n        if (buttonIndex === 0) {\n          event.preventDefault();\n        }\n      });\n    }\n  });\n};\n"
  },
  {
    "path": "main/global-accelerators.ts",
    "content": "import {globalShortcut} from 'electron';\nimport {ipcMain as ipc} from 'electron-better-ipc';\nimport {settings} from './common/settings';\nimport {windowManager} from './windows/manager';\n\nconst openCropper = () => {\n  if (!windowManager.cropper?.isOpen()) {\n    windowManager.cropper?.open();\n  }\n};\n\n// All settings that should be loaded and handled as global accelerators\nconst handlers = new Map<string, () => void>([\n  ['triggerCropper', openCropper]\n]);\n\n// If no action is passed, it resets\nexport const setCropperShortcutAction = (action = openCropper) => {\n  if (settings.get('enableShortcuts') && settings.get('shortcuts.triggerCropper')) {\n    handlers.set('cropperShortcut', action);\n\n    const shortcut = settings.get<string, string>('shortcuts.triggerCropper');\n    if (globalShortcut.isRegistered(shortcut)) {\n      globalShortcut.unregister(shortcut);\n    }\n\n    globalShortcut.register(shortcut, action);\n  }\n};\n\nconst registerShortcut = (shortcut: string, action: () => void) => {\n  try {\n    globalShortcut.register(shortcut, action);\n  } catch (error) {\n    console.error('Error registering shortcut', shortcut, action, error);\n  }\n};\n\nconst registerFromStore = () => {\n  if (settings.get('enableShortcuts')) {\n    for (const [setting, action] of handlers.entries()) {\n      const shortcut = settings.get<string, string>(`shortcuts.${setting}`);\n      if (shortcut) {\n        registerShortcut(shortcut, action);\n      }\n    }\n  } else {\n    globalShortcut.unregisterAll();\n  }\n};\n\nexport const initializeGlobalAccelerators = () => {\n  ipc.answerRenderer('update-shortcut', ({setting, shortcut}) => {\n    const oldShortcut = settings.get<string, string>(`shortcuts.${setting}`);\n\n    try {\n      if (oldShortcut && oldShortcut !== shortcut && globalShortcut.isRegistered(oldShortcut)) {\n        globalShortcut.unregister(oldShortcut);\n      }\n    } catch (error) {\n      console.error('Error unregistering old shortcutAccelerator', error);\n    } finally {\n      if (shortcut && shortcut !== oldShortcut) {\n        settings.set(`shortcuts.${setting}`, shortcut);\n        const handler = handlers.get(setting);\n\n        if (settings.get('enableShortcuts') && handler) {\n          registerShortcut(shortcut, handler);\n        }\n      } else if (!shortcut) {\n        // @ts-expect-error\n        settings.delete(`shortcuts.${setting}`);\n      }\n    }\n  });\n\n  ipc.answerRenderer('toggle-shortcuts', ({enabled}) => {\n    if (enabled) {\n      registerFromStore();\n    } else {\n      globalShortcut.unregisterAll();\n    }\n  });\n\n  // Register keyboard shortcuts from store\n  registerFromStore();\n};\n"
  },
  {
    "path": "main/index.ts",
    "content": "import {app} from 'electron';\nimport {is, enforceMacOSAppLocation} from 'electron-util';\nimport log from 'electron-log';\nimport {autoUpdater} from 'electron-updater';\nimport toMilliseconds from '@sindresorhus/to-milliseconds';\n\nimport './windows/load';\nimport './utils/sentry';\n\nrequire('electron-timber').hookConsole({main: true, renderer: true});\n\nimport {settings} from './common/settings';\nimport {plugins} from './plugins';\nimport {initializeTray} from './tray';\nimport {initializeDevices} from './utils/devices';\nimport {initializeAnalytics, track} from './common/analytics';\nimport {initializeGlobalAccelerators} from './global-accelerators';\nimport {openFiles} from './utils/open-files';\nimport {hasMicrophoneAccess, ensureScreenCapturePermissions} from './common/system-permissions';\nimport {handleDeepLink} from './utils/deep-linking';\nimport {hasActiveRecording, cleanPastRecordings} from './recording-history';\nimport {setupRemoteStates} from './remote-states';\nimport {setUpExportsListeners} from './export';\nimport {windowManager} from './windows/manager';\nimport {setupProtocol} from './utils/protocol';\nimport {stopRecordingWithNoEdit} from './aperture';\n\nconst prepareNext = require('electron-next');\n\nconst filesToOpen: string[] = [];\n\nlet onExitCleanupComplete = false;\n\napp.commandLine.appendSwitch('--enable-features', 'OverlayScrollbar');\n\napp.on('open-file', (event, path) => {\n  event.preventDefault();\n\n  if (app.isReady()) {\n    track('editor/opened/running');\n    openFiles(path);\n  } else {\n    filesToOpen.push(path);\n  }\n});\n\nconst initializePlugins = async () => {\n  if (!is.development) {\n    try {\n      await plugins.upgrade();\n    } catch (error) {\n      console.log(error);\n    }\n  }\n};\n\nconst checkForUpdates = () => {\n  if (is.development) {\n    return false;\n  }\n\n  const checkForUpdates = async () => {\n    try {\n      await autoUpdater.checkForUpdates();\n    } catch (error) {\n      autoUpdater.logger?.error(error);\n    }\n  };\n\n  // For auto-update debugging in Console.app\n  autoUpdater.logger = log;\n  // @ts-expect-error\n  autoUpdater.logger.transports.file.level = 'info';\n\n  setInterval(checkForUpdates, toMilliseconds({hours: 1}));\n\n  checkForUpdates();\n  return true;\n};\n\n// Prepare the renderer once the app is ready\n(async () => {\n  await app.whenReady();\n  require('./utils/errors').setupErrorHandling();\n\n  // Initialize remote states\n  setupRemoteStates();\n\n  setupProtocol();\n\n  app.dock.hide();\n  app.setAboutPanelOptions({copyright: 'Copyright © Wulkano'});\n\n  // Ensure the app is in the Applications folder\n  enforceMacOSAppLocation();\n\n  await prepareNext('./renderer');\n\n  // Ensure all plugins are up to date\n  initializePlugins();\n  initializeDevices();\n  initializeAnalytics();\n  initializeTray();\n  initializeGlobalAccelerators();\n  setUpExportsListeners();\n\n  if (!app.isDefaultProtocolClient('kap')) {\n    app.setAsDefaultProtocolClient('kap');\n  }\n\n  if (filesToOpen.length > 0) {\n    track('editor/opened/startup');\n    openFiles(...filesToOpen);\n    hasActiveRecording();\n  } else if (\n    !(await hasActiveRecording()) &&\n    !app.getLoginItemSettings().wasOpenedAtLogin &&\n    ensureScreenCapturePermissions() &&\n    (!settings.get('recordAudio') || hasMicrophoneAccess())\n  ) {\n    windowManager.cropper?.open();\n  }\n\n  checkForUpdates();\n})();\n\napp.on('window-all-closed', (event: any) => {\n  app.dock.hide();\n  event.preventDefault();\n});\n\napp.on('will-finish-launching', () => {\n  app.on('open-url', (event, url) => {\n    event.preventDefault();\n    handleDeepLink(url);\n  });\n});\n\napp.on('before-quit', async (event: any) => {\n  if (!onExitCleanupComplete) {\n    event.preventDefault();\n    await stopRecordingWithNoEdit();\n    cleanPastRecordings();\n    onExitCleanupComplete = true;\n    app.quit();\n  }\n});\n"
  },
  {
    "path": "main/menus/application.ts",
    "content": "import {appMenu} from 'electron-util';\nimport {getAboutMenuItem, getExportHistoryMenuItem, getOpenFileMenuItem, getPreferencesMenuItem, getSendFeedbackMenuItem} from './common';\nimport {MenuItemId, MenuOptions} from './utils';\n\nconst getAppMenuItem = () => {\n  const appMenuItem = appMenu([getPreferencesMenuItem()]);\n\n  // @ts-expect-error\n  appMenuItem.submenu[0] = getAboutMenuItem();\n  return {...appMenuItem, id: MenuItemId.app};\n};\n\n// eslint-disable-next-line unicorn/prevent-abbreviations\nexport const defaultApplicationMenu = (): MenuOptions => [\n  getAppMenuItem(),\n  {\n    role: 'fileMenu',\n    id: MenuItemId.file,\n    submenu: [\n      getOpenFileMenuItem(),\n      {\n        type: 'separator'\n      },\n      {\n        role: 'close'\n      }\n    ]\n  },\n  {\n    role: 'editMenu',\n    id: MenuItemId.edit\n  },\n  {\n    role: 'windowMenu',\n    id: MenuItemId.window,\n    submenu: [\n      {\n        role: 'minimize'\n      },\n      {\n        role: 'zoom'\n      },\n      {\n        type: 'separator'\n      },\n      getExportHistoryMenuItem(),\n      {\n        type: 'separator'\n      },\n      {\n        role: 'front'\n      }\n    ]\n  },\n  {\n    id: MenuItemId.help,\n    label: 'Help',\n    role: 'help',\n    submenu: [getSendFeedbackMenuItem()]\n  }\n];\n\n// eslint-disable-next-line unicorn/prevent-abbreviations\nexport const customApplicationMenu = (modifier: (defaultMenu: ReturnType<typeof defaultApplicationMenu>) => void) => {\n  const menu = defaultApplicationMenu();\n  modifier(menu);\n  return menu;\n};\n\nexport type MenuModifier = Parameters<typeof customApplicationMenu>[0];\n"
  },
  {
    "path": "main/menus/cog.ts",
    "content": "import {Menu} from 'electron';\nimport {MenuItemId, MenuOptions} from './utils';\nimport {getAboutMenuItem, getExportHistoryMenuItem, getOpenFileMenuItem, getPreferencesMenuItem, getSendFeedbackMenuItem} from './common';\nimport {plugins} from '../plugins';\nimport {getAudioDevices, getDefaultInputDevice} from '../utils/devices';\nimport {settings} from '../common/settings';\nimport {defaultInputDeviceId} from '../common/constants';\nimport {hasMicrophoneAccess} from '../common/system-permissions';\n\nconst getCogMenuTemplate = async (): Promise<MenuOptions> => [\n  getAboutMenuItem(),\n  {\n    type: 'separator'\n  },\n  getPreferencesMenuItem(),\n  {\n    type: 'separator'\n  },\n  getPluginsItem(),\n  await getMicrophoneItem(),\n  {\n    type: 'separator'\n  },\n  getOpenFileMenuItem(),\n  getExportHistoryMenuItem(),\n  {\n    type: 'separator'\n  },\n  getSendFeedbackMenuItem(),\n  {\n    type: 'separator'\n  },\n  {\n    role: 'quit',\n    accelerator: 'Command+Q'\n  }\n];\n\nconst getPluginsItem = (): MenuOptions[number] => {\n  const items = plugins.recordingPlugins.flatMap(plugin =>\n    plugin.recordServicesWithStatus.map(service => ({\n      label: service.title,\n      type: 'checkbox' as const,\n      checked: service.isEnabled,\n      click: async () => service.setEnabled(!service.isEnabled)\n    }))\n  );\n\n  return {\n    id: MenuItemId.plugins,\n    label: 'Plugins',\n    submenu: items,\n    visible: items.length > 0\n  };\n};\n\nconst getMicrophoneItem = async (): Promise<MenuOptions[number]> => {\n  const devices = await getAudioDevices();\n  const isRecordAudioEnabled = settings.get('recordAudio');\n  const currentDefaultDevice = getDefaultInputDevice();\n\n  let audioInputDeviceId = settings.get('audioInputDeviceId');\n  if (!devices.some(device => device.id === audioInputDeviceId)) {\n    settings.set('audioInputDeviceId', defaultInputDeviceId);\n    audioInputDeviceId = defaultInputDeviceId;\n  }\n\n  return {\n    id: MenuItemId.audioDevices,\n    label: 'Microphone',\n    submenu: [\n      {\n        label: 'None',\n        type: 'checkbox',\n        checked: !isRecordAudioEnabled,\n        click: () => {\n          settings.set('recordAudio', false);\n        }\n      },\n      ...[\n        {name: `System Default${currentDefaultDevice ? ` (${currentDefaultDevice.name})` : ''}`, id: defaultInputDeviceId},\n        ...devices\n      ].map(device => ({\n        label: device.name,\n        type: 'checkbox' as const,\n        checked: isRecordAudioEnabled && (audioInputDeviceId === device.id),\n        click: () => {\n          settings.set('recordAudio', true);\n          settings.set('audioInputDeviceId', device.id);\n        }\n      }))\n    ],\n    visible: hasMicrophoneAccess()\n  };\n};\n\nexport const getCogMenu = async () => {\n  return Menu.buildFromTemplate(\n    await getCogMenuTemplate()\n  );\n};\n"
  },
  {
    "path": "main/menus/common.ts",
    "content": "import delay from 'delay';\nimport {app, dialog} from 'electron';\nimport {openNewGitHubIssue} from 'electron-util';\nimport macosRelease from '../utils/macos-release';\nimport {supportedVideoExtensions} from '../common/constants';\nimport {getCurrentMenuItem, MenuItemId} from './utils';\nimport {openFiles} from '../utils/open-files';\nimport {windowManager} from '../windows/manager';\n\nexport const getPreferencesMenuItem = () => ({\n  id: MenuItemId.preferences,\n  label: 'Preferences…',\n  accelerator: 'Command+,',\n  click: () => windowManager.preferences?.open()\n});\n\nexport const getAboutMenuItem = () => ({\n  id: MenuItemId.about,\n  label: `About ${app.name}`,\n  click: () => {\n    windowManager.cropper?.close();\n    app.focus();\n    app.showAboutPanel();\n  }\n});\n\nexport const getOpenFileMenuItem = () => ({\n  id: MenuItemId.openVideo,\n  label: 'Open Video…',\n  accelerator: 'Command+O',\n  click: async () => {\n    windowManager.cropper?.close();\n\n    await delay(200);\n\n    app.focus();\n    const {canceled, filePaths} = await dialog.showOpenDialog({\n      filters: [{name: 'Videos', extensions: supportedVideoExtensions}],\n      properties: ['openFile', 'multiSelections']\n    });\n\n    if (!canceled && filePaths) {\n      openFiles(...filePaths);\n    }\n  }\n});\n\nexport const getExportHistoryMenuItem = () => ({\n  label: 'Export History',\n  click: () => windowManager.exports?.open(),\n  enabled: getCurrentMenuItem(MenuItemId.exportHistory)?.enabled ?? false,\n  id: MenuItemId.exportHistory\n});\n\nexport const getSendFeedbackMenuItem = () => ({\n  id: MenuItemId.sendFeedback,\n  label: 'Send Feedback…',\n  click() {\n    openNewGitHubIssue({\n      user: 'wulkano',\n      repo: 'kap',\n      body: issueBody\n    });\n  }\n});\n\nconst release = macosRelease();\n\nconst issueBody = `\n<!--\nThank you for helping us test Kap. Your feedback helps us make Kap better for everyone!\n\nBefore you continue; please make sure you've searched our existing issues to avoid duplicates. When you're ready to open a new issue, include as much information as possible. You can use the handy template below for bug reports.\n\nStep to reproduce:    If applicable, provide steps to reproduce the issue you're having.\nCurrent behavior:     A description of how Kap is currently behaving.\nExpected behavior:    How you expected Kap to behave.\nWorkaround:           A workaround for the issue if you've found on. (this will help others experiencing the same issue!)\n-->\n\n**macOS version:**    ${release.name} (${release.version})\n**Kap version:**      ${app.getVersion()}\n\n#### Steps to reproduce\n\n#### Current behavior\n\n#### Expected behavior\n\n#### Workaround\n\n<!-- If you have additional information, enter it below. -->\n`;\n\n"
  },
  {
    "path": "main/menus/record.ts",
    "content": "import {Menu} from 'electron';\nimport {MenuItemId, MenuOptions} from './utils';\nimport {pauseRecording, resumeRecording, stopRecording} from '../aperture';\nimport formatTime from '../utils/format-time';\nimport {getCurrentDurationStart, getOverallDuration} from '../utils/track-duration';\n\nconst getDurationLabel = () => {\n  if (getCurrentDurationStart() <= 0) {\n    return formatTime((getOverallDuration()) / 1000, undefined);\n  }\n\n  return formatTime((getOverallDuration() + (Date.now() - getCurrentDurationStart())) / 1000, undefined);\n};\n\nconst getDurationMenuItem = () => ({\n  id: MenuItemId.duration,\n  label: getDurationLabel(),\n  enabled: false\n});\n\nconst getStopRecordingMenuItem = () => ({\n  id: MenuItemId.stopRecording,\n  label: 'Stop',\n  click: stopRecording\n});\n\nconst getPauseRecordingMenuItem = () => ({\n  id: MenuItemId.pauseRecording,\n  label: 'Pause',\n  click: pauseRecording\n});\n\nconst getResumeRecordingMenuItem = () => ({\n  id: MenuItemId.resumeRecording,\n  label: 'Resume',\n  click: resumeRecording\n});\n\nexport const getRecordMenuTemplate = (isPaused: boolean): MenuOptions => [\n  getDurationMenuItem(),\n  {\n    type: 'separator'\n  },\n  isPaused ? getResumeRecordingMenuItem() : getPauseRecordingMenuItem(),\n  getStopRecordingMenuItem(),\n  {\n    type: 'separator'\n  },\n  {\n    role: 'quit',\n    accelerator: 'Command+Q'\n  }\n];\n\nexport const getRecordMenu = async (isPaused: boolean) => {\n  return Menu.buildFromTemplate(getRecordMenuTemplate(isPaused));\n};\n"
  },
  {
    "path": "main/menus/utils.ts",
    "content": "import {Menu} from 'electron';\n\nexport type MenuOptions = Parameters<typeof Menu.buildFromTemplate>[0];\n\nexport enum MenuItemId {\n  exportHistory = 'exportHistory',\n  sendFeedback = 'sendFeedback',\n  openVideo = 'openVideo',\n  about = 'about',\n  preferences = 'preferences',\n  file = 'file',\n  edit = 'edit',\n  window = 'window',\n  help = 'help',\n  app = 'app',\n  saveOriginal = 'saveOriginal',\n  plugins = 'plugins',\n  audioDevices = 'audioDevices',\n  stopRecording = 'stopRecording',\n  pauseRecording = 'pauseRecording',\n  resumeRecording = 'resumeRecording',\n  duration = 'duration'\n}\n\nexport const getCurrentMenuItem = (id: MenuItemId) => {\n  return Menu.getApplicationMenu()?.getMenuItemById(id);\n};\n\nexport const setExportMenuItemState = (enabled: boolean) => {\n  const menuItem = Menu.getApplicationMenu()?.getMenuItemById(MenuItemId.exportHistory);\n\n  if (menuItem) {\n    menuItem.enabled = enabled;\n  }\n};\n"
  },
  {
    "path": "main/plugins/built-in/copy-to-clipboard-plugin.ts",
    "content": "import {clipboard} from 'electron';\nimport {ShareServiceContext} from '../service-context';\n\nconst plist = require('plist');\n\nconst copyFileReferencesToClipboard = (filePaths: string[]) => {\n  clipboard.writeBuffer('NSFilenamesPboardType', Buffer.from(plist.build(filePaths)));\n};\n\nconst action = async (context: ShareServiceContext) => {\n  const filePath = await context.filePath();\n  copyFileReferencesToClipboard([filePath]);\n  context.notify(`The ${context.prettyFormat} has been copied to the clipboard`);\n};\n\nconst copyToClipboard = {\n  title: 'Copy to Clipboard',\n  formats: [\n    'gif',\n    'apng',\n    'mp4'\n  ],\n  action\n};\n\nexport const shareServices = [copyToClipboard];\n"
  },
  {
    "path": "main/plugins/built-in/open-with-plugin.ts",
    "content": "import {ShareServiceContext} from '../service-context';\nimport path from 'path';\nimport {getFormatExtension} from '../../common/constants';\nimport {Format} from '../../common/types';\n\nconst {getAppsThatOpenExtension, openFileWithApp} = require('mac-open-with');\n\nconst action = async (context: ShareServiceContext & {appUrl: string}) => {\n  const filePath = await context.filePath();\n  openFileWithApp(filePath, context.appUrl);\n};\n\nexport interface App {\n  url: string;\n  isDefault: boolean;\n  icon: string;\n  name: string;\n}\n\nconst getAppsForFormat = (format: Format) => {\n  return (getAppsThatOpenExtension.sync(getFormatExtension(format)) as App[])\n    .map(app => ({...app, name: decodeURI(path.parse(app.url).name)}))\n    .filter(app => !['Kap', 'Kap Beta'].includes(app.name))\n    .sort((a, b) => {\n      if (a.isDefault !== b.isDefault) {\n        return Number(b.isDefault) - Number(a.isDefault);\n      }\n\n      return Number(b.name === 'Gifski') - Number(a.name === 'Gifski');\n    });\n};\n\nconst appsForFormat = (['mp4', 'gif', 'apng', 'webm', 'av1', 'hevc'] as Format[])\n  .map(format => ({\n    format,\n    apps: getAppsForFormat(format)\n  }))\n  .filter(({apps}) => apps.length > 0);\n\nexport const apps = new Map(appsForFormat.map(({format, apps}) => [format, apps]));\n\nexport const shareServices = [{\n  title: 'Open With',\n  formats: [...apps.keys()],\n  action\n}];\n"
  },
  {
    "path": "main/plugins/built-in/save-file-plugin.ts",
    "content": "'use strict';\n\nimport {BrowserWindow, dialog} from 'electron';\nimport {ShareServiceContext} from '../service-context';\nimport {settings} from '../../common/settings';\nimport makeDir from 'make-dir';\nimport {Format} from '../../common/types';\nimport path from 'path';\n\nconst {Notification, shell} = require('electron');\nconst cpFile = require('cp-file');\n\nconst action = async (context: ShareServiceContext & {targetFilePath: string}) => {\n  const temporaryFilePath = await context.filePath();\n\n  // Execution has been interrupted\n  if (context.isCanceled) {\n    return;\n  }\n\n  // Copy the file, so we can still use the temporary source for future exports\n  // The temporary file will be cleaned up on app exit, or automatic system cleanup\n  await cpFile(temporaryFilePath, context.targetFilePath);\n\n  const notification = new Notification({\n    title: 'File saved successfully!',\n    body: 'Click to show the file in Finder'\n  });\n\n  notification.on('click', () => {\n    shell.showItemInFolder(context.targetFilePath);\n  });\n\n  notification.show();\n};\n\nconst saveFile = {\n  title: 'Save to Disk',\n  formats: [\n    'gif',\n    'mp4',\n    'webm',\n    'apng',\n    'av1',\n    'hevc'\n  ],\n  action\n};\n\nexport const shareServices = [saveFile];\n\nconst filterMap = new Map([\n  [Format.mp4, [{name: 'Movies', extensions: ['mp4']}]],\n  [Format.webm, [{name: 'Movies', extensions: ['webm']}]],\n  [Format.gif, [{name: 'Images', extensions: ['gif']}]],\n  [Format.apng, [{name: 'Images', extensions: ['apng']}]],\n  [Format.av1, [{name: 'Movies', extensions: ['mp4']}]],\n  [Format.hevc, [{name: 'Movies', extensions: ['mp4']}]]\n]);\n\nlet lastSavedDirectory: string;\n\nexport const askForTargetFilePath = async (\n  window: BrowserWindow,\n  format: Format,\n  fileName: string\n) => {\n  const kapturesDir = settings.get('kapturesDir');\n  await makeDir(kapturesDir);\n\n  const defaultPath = path.join(lastSavedDirectory ?? kapturesDir, fileName);\n\n  const filters = filterMap.get(format);\n\n  const {filePath} = await dialog.showSaveDialog(window, {\n    title: fileName,\n    defaultPath,\n    filters\n  });\n\n  if (filePath) {\n    lastSavedDirectory = path.dirname(filePath);\n    return filePath;\n  }\n\n  return undefined;\n};\n"
  },
  {
    "path": "main/plugins/config.ts",
    "content": "import {ValidateFunction} from 'ajv';\nimport Store, {Schema as JSONSchema} from 'electron-store';\nimport Ajv, {Schema} from '../utils/ajv';\nimport {Service} from './service';\n\nexport default class PluginConfig extends Store {\n  servicesWithNoConfig: Service[];\n  validators: Array<{\n    title: string;\n    description?: string;\n    config: Record<string, Schema>;\n    validate: ValidateFunction;\n  }>;\n\n  constructor(name: string, services: Service[]) {\n    const defaults = {};\n\n    const validators = services\n      .filter(({config}) => Boolean(config))\n      .map(service => {\n        const config = service.config as Record<string, Schema>;\n        const schema: Record<string, JSONSchema<any>> = {};\n        const requiredKeys = [];\n\n        for (const key of Object.keys(config)) {\n          if (!config[key].title) {\n            throw new Error('Config schema items should have a `title`');\n          }\n\n          const {required, ...rest} = config[key];\n\n          if (required) {\n            requiredKeys.push(key);\n          }\n\n          schema[key] = rest;\n        }\n\n        const ajv = new Ajv({\n          format: 'full',\n          useDefaults: true,\n          errorDataPath: 'property',\n          allErrors: true\n        });\n\n        const validator = ajv.compile({\n          type: 'object',\n          properties: schema,\n          required: requiredKeys\n        });\n\n        validator(defaults);\n        return {\n          validate: validator,\n          title: service.title,\n          description: service.configDescription,\n          config\n        };\n      });\n\n    super({\n      name,\n      cwd: 'plugins',\n      defaults\n    });\n\n    this.servicesWithNoConfig = services.filter(({config}) => !config);\n    this.validators = validators;\n  }\n\n  get isValid() {\n    return this.validators.every(validator => validator.validate(this.store));\n  }\n\n  get validServices() {\n    return [\n      ...this.validators.filter(validator => validator.validate(this.store)),\n      ...this.servicesWithNoConfig\n    ].map(service => service.title);\n  }\n}\n"
  },
  {
    "path": "main/plugins/index.ts",
    "content": "import {app} from 'electron';\nimport {EventEmitter} from 'events';\nimport path from 'path';\nimport fs from 'fs';\nimport makeDir from 'make-dir';\nimport execa from 'execa';\nimport {track} from '../common/analytics';\nimport {InstalledPlugin, NpmPlugin} from './plugin';\nimport {showError} from '../utils/errors';\nimport {notify} from '../utils/notifications';\nimport packageJson from 'package-json';\nimport {NormalizedPackageJson} from 'read-pkg';\nimport {windowManager} from '../windows/manager';\n\nconst got = require('got');\n\ntype PackageJson = {\n  dependencies: Record<string, string>;\n};\n\nexport class Plugins extends EventEmitter {\n  yarnBin = path.join(__dirname, '../../node_modules/yarn/bin/yarn.js');\n  appVersion = app.getVersion();\n  pluginsDir = path.join(app.getPath('userData'), 'plugins');\n  builtInDir = path.join(__dirname, 'built-in');\n  packageJsonPath = path.join(this.pluginsDir, 'package.json');\n  installedPlugins: InstalledPlugin[] = [];\n  builtInPlugins = [\n    new InstalledPlugin('_copyToClipboard', path.resolve(this.builtInDir, 'copy-to-clipboard-plugin')),\n    new InstalledPlugin('_saveToDisk', path.resolve(this.builtInDir, 'save-file-plugin')),\n    new InstalledPlugin('_openWith', path.resolve(this.builtInDir, 'open-with-plugin'))\n  ];\n\n  constructor() {\n    super();\n    this.makePluginsDir();\n    this.loadPlugins();\n  }\n\n  async install(name: string): Promise<InstalledPlugin | void> {\n    track(`plugin/installed/${name}`);\n\n    this.modifyMainPackageJson(pkg => {\n      if (!pkg.dependencies) {\n        pkg.dependencies = {};\n      }\n\n      pkg.dependencies[name] = 'latest';\n    });\n\n    try {\n      await this.yarnInstall();\n\n      const plugin = new InstalledPlugin(name);\n      this.installedPlugins.push(plugin);\n\n      if (plugin.content.didInstall && typeof plugin.content.didInstall === 'function') {\n        try {\n          await plugin.content.didInstall?.(plugin.config);\n        } catch (error) {\n          showError(error as any, {plugin} as any);\n        }\n      }\n\n      const {isValid, hasConfig} = plugin;\n\n      // Const openConfig = () => openPrefsWindow({target: {name, action: 'configure'}});\n      const openConfig = () => plugin.openConfig();\n\n      const options = (isValid && !hasConfig) ? {\n        title: 'Plugin installed',\n        body: `\"${plugin.prettyName}\" is ready for use`\n      } : {\n        title: plugin.isValid ? 'Plugin installed' : 'Configure plugin',\n        body: `\"${plugin.prettyName}\" ${plugin.isValid ? 'can be configured' : 'requires configuration'}`,\n        click: openConfig,\n        actions: [\n          {type: 'button' as const, text: 'Configure', action: openConfig},\n          {type: 'button' as const, text: 'Later'}\n        ]\n      };\n\n      notify(options);\n\n      const validServices = plugin.config.validServices;\n\n      for (const service of plugin.recordServices) {\n        if (!service.willEnable && validServices.includes(service.title)) {\n          plugin.enableService(service);\n        }\n      }\n\n      this.emit('installed', plugin);\n      return plugin;\n    } catch (error) {\n      notify.simple(`Something went wrong while installing ${name}`);\n      this.modifyMainPackageJson(pkg => {\n        delete pkg.dependencies[name];\n      });\n      showError(error as any);\n    }\n  }\n\n  async uninstall(name: string) {\n    track(`plugin/uninstalled/${name}`);\n    this.modifyMainPackageJson(pkg => {\n      delete pkg.dependencies[name];\n    });\n    const plugin = new InstalledPlugin(name);\n\n    if (plugin.content.willUninstall && typeof plugin.content.willUninstall === 'function') {\n      try {\n        await plugin.content.willUninstall?.(plugin.config);\n      } catch (error) {\n        showError(error as any, {plugin} as any);\n      }\n    }\n\n    this.installedPlugins = this.installedPlugins.filter(plugin => plugin.name !== name);\n    plugin.config.clear();\n    this.emit('uninstalled', name);\n\n    const json = plugin.json!;\n\n    return new NpmPlugin(json, {\n      version: json.kapVersion,\n      ...json.kap\n    });\n  }\n\n  async upgrade() {\n    return this.yarnInstall();\n  }\n\n  async getFromNpm() {\n    const url = 'https://api.npms.io/v2/search?q=keywords:kap-plugin+not:deprecated';\n    const response = (await got(url, {json: true})) as {\n      body: {results: Array<{package: NormalizedPackageJson}>};\n    };\n    const installed = this.pluginNames;\n\n    return Promise.all(response.body.results\n      .map(x => x.package)\n      .filter(x => x.name.startsWith('kap-'))\n      .filter(x => !installed.includes(x.name)) // Filter out installed plugins\n      .map(async x => {\n        const {kap, kapVersion} = await packageJson(x.name, {fullMetadata: true}) as any;\n        return new NpmPlugin(x, {\n          // Keeping for backwards compatibility\n          version: kapVersion,\n          ...kap\n        });\n      }));\n  }\n\n  get allPlugins() {\n    return [\n      ...this.installedPlugins,\n      ...this.builtInPlugins\n    ];\n  }\n\n  get sharePlugins() {\n    return this.allPlugins.filter(plugin => plugin.shareServices.length > 0);\n  }\n\n  get editPlugins() {\n    return this.allPlugins.filter(plugin => plugin.editServices.length > 0);\n  }\n\n  get recordingPlugins() {\n    return this.allPlugins.filter(plugin => plugin.recordServices.length > 0);\n  }\n\n  openPluginConfig = async (pluginName: string) => {\n    return windowManager.config?.open(pluginName);\n  };\n\n  private makePluginsDir() {\n    if (!fs.existsSync(this.packageJsonPath)) {\n      makeDir.sync(this.pluginsDir);\n      fs.writeFileSync(this.packageJsonPath, JSON.stringify({dependencies: {}}, null, 2));\n    }\n  }\n\n  private modifyMainPackageJson(modifier: (pkg: PackageJson) => void) {\n    const pkg = JSON.parse(fs.readFileSync(this.packageJsonPath, 'utf8'));\n    modifier(pkg);\n    fs.writeFileSync(this.packageJsonPath, JSON.stringify(pkg, null, 2));\n  }\n\n  private async runYarn(...args: string[]) {\n    await execa(process.execPath, [this.yarnBin, ...args], {\n      cwd: this.pluginsDir,\n      env: {\n        ELECTRON_RUN_AS_NODE: '1',\n        NODE_ENV: 'development'\n      }\n    });\n  }\n\n  private get pluginNames() {\n    const pkg = fs.readFileSync(this.packageJsonPath, 'utf8');\n    return Object.keys(JSON.parse(pkg).dependencies || {});\n  }\n\n  private async yarnInstall() {\n    await this.runYarn('install', '--no-lockfile', '--registry', 'https://registry.npmjs.org');\n  }\n\n  private loadPlugins() {\n    this.installedPlugins = this.pluginNames.map(name => new InstalledPlugin(name));\n  }\n}\n\nexport const plugins = new Plugins();\n"
  },
  {
    "path": "main/plugins/plugin.ts",
    "content": "import {app, shell} from 'electron';\nimport macosVersion from 'macos-version';\nimport semver from 'semver';\nimport path from 'path';\nimport fs from 'fs';\nimport readPkg from 'read-pkg';\nimport {RecordService, ShareService, EditService} from './service';\nimport {showError} from '../utils/errors';\nimport PluginConfig from './config';\nimport Store from 'electron-store';\nimport {windowManager} from '../windows/manager';\n\nexport const recordPluginServiceState = new Store<Record<string, boolean>>({\n  name: 'record-plugin-state',\n  defaults: {}\n});\n\nclass BasePlugin {\n  name: string;\n  kapVersion?: string;\n  macosVersion?: string;\n  link?: string;\n  json?: readPkg.NormalizedPackageJson;\n\n  constructor(pluginName: string) {\n    this.name = pluginName;\n  }\n\n  get prettyName() {\n    return this.name.replace(/^kap-/, '');\n  }\n\n  get isCompatible() {\n    return semver.satisfies(app.getVersion(), this.kapVersion ?? '*') && macosVersion.is(this.macosVersion ?? '*');\n  }\n\n  get repoUrl() {\n    if (!this.link) {\n      return '';\n    }\n\n    const url = new URL(this.link);\n    url.hash = '';\n    return url.href;\n  }\n\n  get version() {\n    return this.json?.version;\n  }\n\n  get description() {\n    return this.json?.description;\n  }\n\n  viewOnGithub() {\n    if (this.link) {\n      shell.openExternal(this.link);\n    }\n  }\n}\n\nexport interface KapPlugin<Config = any> {\n  shareServices?: Array<ShareService<Config>>;\n  editServices?: Array<EditService<Config>>;\n  recordServices?: Array<RecordService<Config>>;\n\n  didConfigChange?: (newValue: Readonly<any> | undefined, oldValue: Readonly<any> | undefined, config: Store<Config>) => void | Promise<void>;\n  didInstall?: (config: Store<Config>) => void | Promise<void>;\n  willUninstall?: (config: Store<Config>) => void | Promise<void>;\n}\n\nexport class InstalledPlugin extends BasePlugin {\n  isInstalled = true;\n  pluginsPath = path.join(app.getPath('userData'), 'plugins');\n\n  pluginPath: string;\n  json?: readPkg.NormalizedPackageJson;\n  content: KapPlugin;\n  config: PluginConfig;\n  hasConfig: boolean;\n  isBuiltIn: boolean;\n\n  constructor(pluginName: string, customPath?: string) {\n    super(pluginName);\n\n    this.pluginPath = customPath ?? path.join(this.pluginsPath, 'node_modules', pluginName);\n    this.isBuiltIn = Boolean(customPath);\n\n    if (!this.isBuiltIn) {\n      this.json = readPkg.sync({cwd: this.pluginPath});\n      this.link = this.json.homepage ?? this.json.links?.homepage;\n\n      // Keeping for backwards compatibility\n      this.kapVersion = this.json.kap?.version ?? this.json.kapVersion;\n      this.macosVersion = this.json.kap?.macosVersion;\n    }\n\n    try {\n      this.content = require(this.pluginPath);\n      this.config = new PluginConfig(pluginName, this.allServices);\n      this.hasConfig = this.allServices.some(({config = {}}) => Object.keys(config).length > 0);\n\n      if (this.content.didConfigChange && typeof this.content.didConfigChange === 'function') {\n        this.config.onDidAnyChange((newValue, oldValue) => this.content.didConfigChange?.(newValue, oldValue, this.config));\n      }\n    } catch (error) {\n      showError(error as any, {title: `Something went wrong while loading “${pluginName}”`, plugin: this});\n\n      this.content = {};\n      this.config = new PluginConfig(pluginName, []);\n      this.hasConfig = false;\n    }\n  }\n\n  get isSymLink() {\n    return fs.lstatSync(this.pluginPath).isSymbolicLink();\n  }\n\n  get shareServices() {\n    return this.content.shareServices ?? [];\n  }\n\n  get editServices() {\n    return this.content.editServices ?? [];\n  }\n\n  get recordServices() {\n    return this.content.recordServices ?? [];\n  }\n\n  get allServices() {\n    return [\n      ...this.shareServices,\n      ...this.editServices,\n      ...this.recordServices\n    ];\n  }\n\n  get isValid() {\n    return this.config.isValid;\n  }\n\n  get recordServicesWithStatus() {\n    return this.recordServices.map(service => ({\n      ...service,\n      isEnabled: recordPluginServiceState.get(this.getRecordServiceKey(service), false),\n      setEnabled: this.getSetEnableFunction(service)\n    }));\n  }\n\n  enableService = (service: RecordService) => {\n    recordPluginServiceState.set(this.getRecordServiceKey(service), true);\n  };\n\n  openConfig = () => windowManager.config?.open(this.name);\n\n  openConfigInEditor = () => {\n    return this.config.openInEditor();\n  };\n\n  private readonly getSetEnableFunction = (service: RecordService) => async (enabled: boolean) => {\n    const isEnabled = recordPluginServiceState.get(this.getRecordServiceKey(service), false);\n\n    if (isEnabled === enabled) {\n      return;\n    }\n\n    if (!enabled) {\n      recordPluginServiceState.set(this.getRecordServiceKey(service), false);\n      return;\n    }\n\n    if (!this.config.validServices.includes(service.title)) {\n      windowManager.preferences?.open({target: {name: this.name, action: 'configure'}});\n      return;\n    }\n\n    if (service.willEnable && typeof service.willEnable === 'function') {\n      try {\n        const canEnable = await service.willEnable();\n\n        if (canEnable) {\n          recordPluginServiceState.set(this.getRecordServiceKey(service), true);\n        }\n      } catch (error) {\n        showError(error as any, {title: `Something went wrong while enabling \"${service.title}`, plugin: this});\n      }\n    } else {\n      recordPluginServiceState.set(this.getRecordServiceKey(service), true);\n    }\n  };\n\n  private readonly getRecordServiceKey = (service: RecordService) => `${this.name}-${service.title}`;\n}\n\nexport class NpmPlugin extends BasePlugin {\n  isInstalled = false;\n\n  constructor(json: readPkg.NormalizedPackageJson, kap: {version?: string; macosVersion?: string} = {}) {\n    super(json.name);\n\n    this.json = json;\n    this.kapVersion = kap.version;\n    this.macosVersion = kap.macosVersion;\n    this.link = this.json.homepage ?? this.json.links?.homepage;\n  }\n}\n"
  },
  {
    "path": "main/plugins/service-context.ts",
    "content": "import {app, clipboard} from 'electron';\nimport Store from 'electron-store';\nimport got, {GotFn, GotPromise} from 'got';\nimport {ApertureOptions, Format} from '../common/types';\nimport {InstalledPlugin} from './plugin';\nimport {addPluginPromise} from '../utils/deep-linking';\nimport {notify} from '../utils/notifications';\nimport PCancelable from 'p-cancelable';\nimport {getFormatExtension} from '../common/constants';\n\ninterface ServiceContextOptions {\n  plugin: InstalledPlugin;\n}\n\nclass ServiceContext {\n  requests: Array<GotPromise<any>> = [];\n  config: Store;\n\n  private readonly plugin: InstalledPlugin;\n\n  constructor(options: ServiceContextOptions) {\n    this.plugin = options.plugin;\n    this.config = this.plugin.config;\n  }\n\n  request = (...args: Parameters<GotFn>) => {\n    const request = got(...args);\n    this.requests.push(request);\n    return request;\n  };\n\n  copyToClipboard = (text: string) => {\n    clipboard.writeText(text);\n  };\n\n  notify = (text: string, action?: () => any) => {\n    return notify({\n      body: text,\n      title: this.plugin.isBuiltIn ? app.name : this.plugin.prettyName,\n      click: action\n    });\n  };\n\n  openConfigFile = () => {\n    this.config.openInEditor();\n  };\n\n  waitForDeepLink = async () => {\n    return new Promise(resolve => {\n      addPluginPromise(this.plugin.name, resolve);\n    });\n  };\n}\n\ninterface ShareServiceContextOptions extends ServiceContextOptions {\n  onProgress: (text: string, percentage: number) => void;\n  filePath: (options?: {fileType?: Format}) => Promise<string>;\n  format: Format;\n  prettyFormat: string;\n  defaultFileName: string;\n  onCancel: () => void;\n}\n\nexport class ShareServiceContext extends ServiceContext {\n  isCanceled = false;\n\n  private readonly options: ShareServiceContextOptions;\n\n  constructor(options: ShareServiceContextOptions) {\n    super(options);\n    this.options = options;\n  }\n\n  get format() {\n    return this.options.format;\n  }\n\n  get prettyFormat() {\n    return this.options.prettyFormat;\n  }\n\n  get defaultFileName() {\n    return `${this.options.defaultFileName}.${getFormatExtension(this.options.format)}`;\n  }\n\n  filePath = async (options?: {fileType?: Format}) => {\n    return this.options.filePath(options);\n  };\n\n  setProgress = (text: string, percentage: number) => {\n    this.options.onProgress(text, percentage);\n  };\n\n  cancel = () => {\n    this.isCanceled = true;\n    this.options.onCancel();\n\n    for (const request of this.requests) {\n      request.cancel();\n    }\n  };\n}\n\ninterface EditServiceContextOptions extends ServiceContextOptions {\n  onProgress: (text: string, percentage: number) => void;\n  inputPath: string;\n  outputPath: string;\n  exportOptions: {\n    width: number;\n    height: number;\n    format: Format;\n    fps: number;\n    duration: number;\n    isMuted: boolean;\n    loop: boolean;\n  };\n  convert: (args: string[], text?: string) => PCancelable<void>;\n  onCancel: () => void;\n}\n\nexport class EditServiceContext extends ServiceContext {\n  isCanceled = false;\n\n  private readonly options: EditServiceContextOptions;\n\n  constructor(options: EditServiceContextOptions) {\n    super(options);\n    this.options = options;\n  }\n\n  get inputPath() {\n    return this.options.inputPath;\n  }\n\n  get outputPath() {\n    return this.options.outputPath;\n  }\n\n  get exportOptions() {\n    return this.options.exportOptions;\n  }\n\n  get convert() {\n    return this.options.convert;\n  }\n\n  setProgress = (text: string, percentage: number) => {\n    this.options.onProgress(text, percentage);\n  };\n\n  cancel = () => {\n    this.isCanceled = true;\n    this.options.onCancel();\n\n    for (const request of this.requests) {\n      request.cancel();\n    }\n  };\n}\n\nexport type RecordServiceState<PersistedState extends Record<string, unknown> = Record<string, unknown>> = {\n  persistedState?: PersistedState;\n};\n\nexport interface RecordServiceContextOptions<State extends RecordServiceState> extends ServiceContextOptions {\n  apertureOptions: ApertureOptions;\n  state: State;\n  setRecordingName: (name: string) => void;\n}\n\nexport class RecordServiceContext<State extends RecordServiceState> extends ServiceContext {\n  private readonly options: RecordServiceContextOptions<State>;\n\n  constructor(options: RecordServiceContextOptions<State>) {\n    super(options);\n    this.options = options;\n  }\n\n  get state() {\n    return this.options.state;\n  }\n\n  get apertureOptions() {\n    return this.options.apertureOptions;\n  }\n\n  get setRecordingName() {\n    return this.options.setRecordingName;\n  }\n}\n"
  },
  {
    "path": "main/plugins/service.ts",
    "content": "\nimport PCancelable from 'p-cancelable';\nimport {Format} from '../common/types';\nimport {Schema} from '../utils/ajv';\nimport {EditServiceContext, RecordServiceContext, ShareServiceContext} from './service-context';\n\nexport interface Service<Config = any> {\n  title: string;\n  configDescription?: string;\n  config?: {[P in keyof Config]: Schema};\n}\n\nexport interface ShareService<Config = any> extends Service<Config> {\n  formats: Format[];\n  action: (context: ShareServiceContext) => PromiseLike<void> | PCancelable<void>;\n}\n\nexport interface EditService<Config = any> extends Service<Config> {\n  action: (context: EditServiceContext) => PromiseLike<void> | PCancelable<void>;\n}\n\nexport type RecordServiceHook = 'willStartRecording' | 'didStartRecording' | 'didStopRecording';\n\nexport type RecordService<Config = any> = Service<Config> & {\n  [key in RecordServiceHook]: ((context: RecordServiceContext<any>) => PromiseLike<void>) | undefined;\n} & {\n  willEnable?: () => PromiseLike<boolean>;\n  cleanUp?: (persistedState: Record<string, unknown>) => void;\n};\n"
  },
  {
    "path": "main/recording-history.ts",
    "content": "/* eslint-disable array-element-newline */\n'use strict';\n\nimport {shell, clipboard} from 'electron';\nimport fs from 'fs';\nimport Store from 'electron-store';\nimport execa from 'execa';\nimport tempy from 'tempy';\nimport {SetOptional} from 'type-fest';\n\nimport {windowManager} from './windows/manager';\nimport {plugins} from './plugins';\nimport {generateTimestampedName} from './utils/timestamped-name';\nimport {Video} from './video';\nimport {ApertureOptions} from './common/types';\nimport Sentry, {isSentryEnabled} from './utils/sentry';\n\nimport ffmpegPath from './utils/ffmpeg-path';\n\nexport interface PastRecording {\n  filePath: string;\n  name: string;\n  date: string;\n}\n\nexport interface ActiveRecording extends PastRecording {\n  apertureOptions: ApertureOptions;\n  plugins: Record<string, Record<string, any>>;\n}\n\nexport const recordingHistory = new Store<{\n  activeRecording: ActiveRecording;\n  recordings: PastRecording[];\n}>({\n  name: 'recording-history',\n  schema: {\n    activeRecording: {\n      type: 'object',\n      properties: {\n        filePath: {\n          type: 'string'\n        },\n        name: {\n          type: 'string'\n        },\n        date: {\n          type: 'string'\n        },\n        apertureOptions: {\n          type: 'object'\n        },\n        plugins: {\n          type: 'object'\n        }\n      }\n    },\n    recordings: {\n      type: 'array',\n      default: [],\n      items: {\n        type: 'object',\n        properties: {\n          filePath: {\n            type: 'string'\n          },\n          name: {\n            type: 'string'\n          },\n          date: {\n            type: 'string'\n          }\n        }\n      }\n    }\n  }\n});\n\nexport const setCurrentRecording = ({\n  filePath,\n  name = generateTimestampedName(),\n  date = new Date().toISOString(),\n  apertureOptions,\n  plugins = {}\n}: SetOptional<ActiveRecording, 'name' | 'date'>) => {\n  recordingHistory.set('activeRecording', {\n    filePath,\n    name,\n    date,\n    apertureOptions,\n    plugins\n  });\n};\n\nexport const updatePluginState = (state: ActiveRecording['plugins']) => {\n  recordingHistory.set('activeRecording.plugins', state);\n};\n\nexport const stopCurrentRecording = (recordingName?: string) => {\n  const {filePath, name} = recordingHistory.get('activeRecording');\n  addRecording({\n    filePath,\n    name: recordingName ?? name,\n    date: new Date().toISOString()\n  });\n  recordingHistory.delete('activeRecording');\n};\n\nexport const getPastRecordings = (): PastRecording[] => {\n  const recordings = recordingHistory.get('recordings', []);\n  const validRecordings = recordings.filter(({filePath}) => fs.existsSync(filePath));\n  recordingHistory.set('recordings', validRecordings);\n  return validRecordings;\n};\n\nexport const addRecording = (newRecording: PastRecording): PastRecording[] => {\n  const recordings = [newRecording, ...recordingHistory.get('recordings', [])];\n  const validRecordings = recordings.filter(({filePath}) => fs.existsSync(filePath));\n  recordingHistory.set('recordings', validRecordings);\n  return validRecordings;\n};\n\nexport const cleanPastRecordings = () => {\n  const recordings = getPastRecordings();\n  for (const recording of recordings) {\n    fs.unlinkSync(recording.filePath);\n  }\n\n  recordingHistory.set('recordings', []);\n};\n\nexport const cleanUpRecordingPlugins = (usedPlugins: ActiveRecording['plugins']) => {\n  const recordingPlugins = plugins.recordingPlugins;\n\n  for (const pluginName of Object.keys(usedPlugins)) {\n    const plugin = recordingPlugins.find(p => p.name === pluginName);\n    for (const [serviceTitle, persistedState] of Object.entries(usedPlugins[pluginName])) {\n      const service = plugin?.recordServices.find(s => s.title === serviceTitle);\n\n      if (service?.cleanUp) {\n        service.cleanUp(persistedState);\n      }\n    }\n  }\n};\n\nexport const handleIncompleteRecording = async (recording: ActiveRecording) => {\n  cleanUpRecordingPlugins(recording.plugins);\n\n  try {\n    await execa(ffmpegPath, [\n      '-i', recording.filePath,\n      // Verbosity level\n      '-v', 'error',\n      // Force file type to null (we don't want to actually generate a file)\n      // https://trac.ffmpeg.org/wiki/Null\n      '-f', 'null', '-'\n    ]);\n  } catch (error) {\n    return handleCorruptRecording(recording, (error as any).stderr);\n  }\n\n  return handleRecording(recording);\n};\n\nconst handleRecording = async (recording: ActiveRecording) => {\n  addRecording({\n    filePath: recording.filePath,\n    name: recording.name,\n    date: recording.date\n  });\n\n  return windowManager.dialog?.open({\n    title: 'Kap didn\\'t shut down correctly.',\n    detail: 'Looks like Kap crashed during a recording. Kap was able to locate the file and it appears to be playable.',\n    buttons: [\n      'Close',\n      {\n        label: 'Show in Finder',\n        action: () => {\n          shell.showItemInFolder(recording.filePath);\n        }\n      },\n      {\n        label: 'Show in Editor',\n        action: async () => Video.getOrCreate({filePath: recording.filePath, title: recording.name}).openEditorWindow()\n      }\n    ]\n  });\n};\n\nconst knownErrors = [{\n  test: (error: string) => error.includes('moov atom not found'),\n  fix: async (filePath: string): Promise<string | void> => {\n    try {\n      const outputPath = tempy.file({extension: 'mp4'});\n\n      await execa(ffmpegPath, [\n        '-i',\n        filePath,\n        // Copy both streams\n        '-vcodec',\n        'copy',\n        '-acodec',\n        'copy',\n        // Attempt to move the moov atom to the start of the file\n        '-movflags',\n        'faststart',\n        outputPath\n      ]);\n\n      return outputPath;\n    } catch {}\n  }\n}];\n\nconst handleCorruptRecording = async (recording: ActiveRecording, error: string) => {\n  const options: any = {\n    title: 'Kap didn\\'t shut down correctly.',\n    detail: `Looks like Kap crashed during a recording. We were able to locate the file. Unfortunately, it appears to be corrupt.\\n\\n${error}`,\n    cancelId: 0,\n    defaultId: 2,\n    buttons: [\n      'Close',\n      {\n        label: 'Copy Error',\n        action: () => {\n          clipboard.writeText(error);\n        }\n      },\n      {\n        label: 'Show in Finder',\n        action: () => {\n          shell.showItemInFolder(recording.filePath);\n        }\n      }\n    ]\n  };\n\n  const applicableErrors = knownErrors.filter(({test}) => test(error));\n\n  if (applicableErrors.length === 0) {\n    if (isSentryEnabled) {\n      // Collect info about possible unknown errors, to see if we can implement fixes using ffmpeg\n      Sentry.captureException(new Error(`Corrupt recording: ${error}`));\n    }\n\n    return windowManager.dialog?.open(options);\n  }\n\n  options.message = 'We can attempt to repair the recording.';\n  options.defaultId = 3;\n  options.buttons.push({\n    label: 'Attempt to Fix',\n    activeLabel: 'Attempting to Fix…',\n    action: async (_: any, updateUi: any) => {\n      for (const {fix} of applicableErrors) {\n        const outputPath = await fix(recording.filePath);\n\n        if (outputPath) {\n          addRecording({\n            filePath: outputPath,\n            name: recording.name,\n            date: new Date().toISOString()\n          });\n\n          return updateUi({\n            message: 'The recording was successfully repaired.',\n            defaultId: 2,\n            buttons: [\n              'Close',\n              {\n                label: 'Show in Finder',\n                action: () => {\n                  shell.showItemInFolder(outputPath);\n                }\n              },\n              {\n                label: 'Show in Editor',\n                action: async () => Video.getOrCreate({filePath: outputPath, title: recording.name}).openEditorWindow()\n              }\n            ]\n          });\n        }\n      }\n\n      return updateUi({\n        message: 'Kap was unable to repair the recording.',\n        defaultId: 2,\n        buttons: [\n          'Close',\n          {\n            label: 'Copy Error',\n            action: () => {\n              clipboard.writeText(error);\n            }\n          },\n          {\n            label: 'Show in Finder',\n            action: () => {\n              shell.showItemInFolder(recording.filePath);\n            }\n          }\n        ]\n      });\n    }\n  });\n\n  return windowManager.dialog?.open(options);\n};\n\nexport const hasActiveRecording = async () => {\n  const activeRecording = recordingHistory.get('activeRecording');\n\n  if (activeRecording) {\n    await handleIncompleteRecording(activeRecording);\n    recordingHistory.delete('activeRecording');\n    return true;\n  }\n\n  return false;\n};\n"
  },
  {
    "path": "main/remote-states/editor-options.ts",
    "content": "import Store from 'electron-store';\nimport {EditorOptionsRemoteState, ExportOptions, ExportOptionsPlugin, Format, RemoteStateHandler} from '../common/types';\nimport {formats} from '../common/constants';\n\nimport {plugins} from '../plugins';\nimport {apps} from '../plugins/built-in/open-with-plugin';\nimport {prettifyFormat} from '../utils/formats';\n\nconst exportUsageHistory = new Store<{[key in Format]: {lastUsed: number; plugins: Record<string, number>}}>({\n  name: 'export-usage-history',\n  defaults: {\n    gif: {lastUsed: 6, plugins: {default: 1}},\n    mp4: {lastUsed: 5, plugins: {default: 1}},\n    webm: {lastUsed: 4, plugins: {default: 1}},\n    hevc: {lastUsed: 3, plugins: {default: 1}},\n    av1: {lastUsed: 2, plugins: {default: 1}},\n    apng: {lastUsed: 1, plugins: {default: 1}}\n  }\n});\n\nconst fpsUsageHistory = new Store<{[key in Format]: number}>({\n  name: 'fps-usage-history',\n  schema: {\n    apng: {\n      type: 'number',\n      minimum: 0,\n      default: 60\n    },\n    webm: {\n      type: 'number',\n      minimum: 0,\n      default: 60\n    },\n    mp4: {\n      type: 'number',\n      minimum: 0,\n      default: 60\n    },\n    gif: {\n      type: 'number',\n      minimum: 0,\n      default: 60\n    },\n    av1: {\n      type: 'number',\n      minimum: 0,\n      default: 60\n    },\n    hevc: {\n      type: 'number',\n      minimum: 0,\n      default: 60\n    }\n  }\n});\n\nconst getEditOptions = () => {\n  return plugins.editPlugins.flatMap(\n    plugin => plugin.editServices\n      .filter(service => plugin.config.validServices.includes(service.title))\n      .map(service => ({\n        title: service.title,\n        pluginName: plugin.name,\n        pluginPath: plugin.pluginPath,\n        hasConfig: Object.keys(service.config ?? {}).length > 0\n      }))\n  );\n};\n\nconst getExportOptions = () => {\n  const installed = plugins.sharePlugins;\n\n  const options = formats.map(format => ({\n    format,\n    prettyFormat: prettifyFormat(format),\n    plugins: [] as ExportOptionsPlugin[],\n    lastUsed: exportUsageHistory.get(format).lastUsed\n  }));\n\n  const sortFunc = <T extends {lastUsed: number}>(a: T, b: T) => b.lastUsed - a.lastUsed;\n\n  for (const plugin of installed) {\n    if (!plugin.isCompatible) {\n      continue;\n    }\n\n    for (const service of plugin.shareServices) {\n      for (const format of service.formats) {\n        options.find(option => option.format === format)?.plugins.push({\n          title: service.title,\n          pluginName: plugin.name,\n          pluginPath: plugin.pluginPath,\n          apps: plugin.name === '_openWith' ? apps.get(format) : undefined,\n          lastUsed: exportUsageHistory.get(format).plugins?.[plugin.name] ?? 0\n        });\n      }\n    }\n  }\n\n  return options.map(option => ({...option, plugins: option.plugins.sort(sortFunc)})).sort(sortFunc);\n};\n\nconst editorOptionsRemoteState: RemoteStateHandler<EditorOptionsRemoteState> = sendUpdate => {\n  const state: ExportOptions = {\n    formats: getExportOptions(),\n    editServices: getEditOptions(),\n    fpsHistory: fpsUsageHistory.store\n  };\n\n  const updatePlugins = () => {\n    state.formats = getExportOptions();\n    state.editServices = getEditOptions();\n    sendUpdate(state);\n  };\n\n  plugins.on('installed', updatePlugins);\n  plugins.on('uninstalled', updatePlugins);\n  plugins.on('config-changed', updatePlugins);\n\n  const actions = {\n    updatePluginUsage: (_: string, {format, plugin}: {format: Format; plugin: string}) => {\n      const usage = exportUsageHistory.get(format);\n      const now = Date.now();\n\n      usage.plugins[plugin] = now;\n      usage.lastUsed = now;\n      exportUsageHistory.set(format, usage);\n\n      state.formats = getExportOptions();\n      sendUpdate(state);\n    },\n    updateFpsUsage: (_: string, {format, fps}: {format: Format; fps: number}) => {\n      fpsUsageHistory.set(format, fps);\n      state.fpsHistory = fpsUsageHistory.store;\n      sendUpdate(state);\n    }\n  };\n\n  return {\n    actions,\n    getState: () => state\n  };\n};\n\nexport default editorOptionsRemoteState;\nexport const name = 'editor-options';\n"
  },
  {
    "path": "main/remote-states/exports-list.ts",
    "content": "import {ExportsListRemoteState, RemoteStateHandler} from '../common/types';\nimport Export from '../export';\n\nconst exportsListRemoteState: RemoteStateHandler<ExportsListRemoteState> = sendUpdate => {\n  const getState = () => {\n    return [...Export.exportsMap.keys()];\n  };\n\n  const subscribe = () => {\n    const callback = () => {\n      sendUpdate([...Export.exportsMap.keys()]);\n    };\n\n    Export.events.on('added', callback);\n    return () => {\n      Export.events.off('added', callback);\n    };\n  };\n\n  return {\n    subscribe,\n    getState,\n    actions: {}\n  };\n};\n\nexport default exportsListRemoteState;\nexport const name = 'exports-list';\n"
  },
  {
    "path": "main/remote-states/exports.ts",
    "content": "import {shell} from 'electron';\nimport {ExportsRemoteState, RemoteStateHandler} from '../common/types';\nimport Export from '../export';\n\nconst exportsRemoteState: RemoteStateHandler<ExportsRemoteState> = sendUpdate => {\n  const getState = (exportId: string) => {\n    const exportInstance = Export.fromId(exportId);\n\n    if (!exportInstance) {\n      return;\n    }\n\n    return exportInstance.data;\n  };\n\n  const subscribe = (exportId: string) => {\n    const exportInstance = Export.fromId(exportId);\n\n    if (!exportInstance) {\n      return;\n    }\n\n    const callback = () => {\n      sendUpdate(exportInstance.data, exportId);\n    };\n\n    exportInstance.on('updated', callback);\n    return () => {\n      exportInstance.off('updated', callback);\n    };\n  };\n\n  const actions = {\n    cancel: (exportId: string) => {\n      Export.fromId(exportId)?.cancel();\n    },\n    copy: (exportId: string) => {\n      Export.fromId(exportId)?.conversion?.copy();\n    },\n    retry: (exportId: string) => {\n      Export.fromId(exportId)?.retry();\n    },\n    openInEditor: (exportId: string) => {\n      Export.fromId(exportId)?.video?.openEditorWindow?.();\n    },\n    showInFolder: (exportId: string) => {\n      const exportInstance = Export.fromId(exportId);\n\n      if (!exportInstance) {\n        return;\n      }\n\n      if (exportInstance.finalFilePath && !exportInstance.data.disableOutputActions) {\n        shell.showItemInFolder(exportInstance.finalFilePath);\n      }\n    }\n  } as any;\n\n  return {\n    subscribe,\n    getState,\n    actions\n  };\n};\n\nexport default exportsRemoteState;\nexport const name = 'exports';\n"
  },
  {
    "path": "main/remote-states/index.ts",
    "content": "import setupRemoteState from './setup-remote-state';\n\nconst remoteStateNames = ['editor-options', 'exports', 'exports-list'];\n\nexport const setupRemoteStates = async () => {\n  return Promise.all(remoteStateNames.map(async fileName => {\n    const state = require(`./${fileName}`);\n    console.log(`Setting up remote-state: ${state.name}`);\n    setupRemoteState(state.name, state.default);\n  }));\n};\n"
  },
  {
    "path": "main/remote-states/setup-remote-state.ts",
    "content": "import {RemoteState, getChannelNames} from './utils';\nimport {ipcMain} from 'electron-better-ipc';\nimport {BrowserWindow} from 'electron';\n\n// eslint-disable-next-line @typescript-eslint/ban-types\nconst setupRemoteState = async <State, Actions extends Record<string, Function>>(name: string, callback: RemoteState<State, Actions>) => {\n  const channelNames = getChannelNames(name);\n\n  const renderersMap = new Map<string, Set<BrowserWindow>>();\n\n  const sendUpdate = async (state?: State, id?: string) => {\n    if (id) {\n      const renderers = renderersMap.get(id) ?? new Set();\n\n      for (const renderer of renderers) {\n        ipcMain.callRenderer(renderer, channelNames.stateUpdated, {state, id});\n      }\n\n      return;\n    }\n\n    for (const [windowId, renderers] of renderersMap.entries()) {\n      for (const renderer of renderers) {\n        if (renderer && !renderer.isDestroyed()) {\n          ipcMain.callRenderer(renderer, channelNames.stateUpdated, {state: state ?? (await getState?.(windowId))});\n        } else {\n          renderers.delete(renderer);\n        }\n      }\n    }\n  };\n\n  const {getState, actions = {}, subscribe} = await callback(sendUpdate);\n\n  ipcMain.answerRenderer(channelNames.subscribe, (customId: string, window: BrowserWindow) => {\n    const id = customId ?? window.id.toString();\n\n    if (!renderersMap.has(id)) {\n      renderersMap.set(id, new Set());\n    }\n\n    renderersMap.get(id)?.add(window);\n    const unsubscribe = subscribe?.(id);\n\n    window.on('close', () => {\n      renderersMap.get(id)?.delete(window);\n      unsubscribe?.();\n    });\n\n    return Object.keys(actions);\n  });\n\n  ipcMain.answerRenderer(channelNames.getState, async (customId: string, window: BrowserWindow) => {\n    const id = customId ?? window.id.toString();\n    return getState(id);\n  });\n\n  ipcMain.answerRenderer(channelNames.callAction, ({key, data, id: customId}: any, window: BrowserWindow) => {\n    const id = customId || window.id.toString();\n    return (actions as any)[key]?.(id, ...data);\n  });\n};\n\nexport default setupRemoteState;\n"
  },
  {
    "path": "main/remote-states/utils.ts",
    "content": "import {Promisable} from 'type-fest';\n\nexport const getChannelName = (name: string, action: string) => `kap-remote-state-${name}-${action}`;\n\nexport const getChannelNames = (name: string) => ({\n  subscribe: getChannelName(name, 'subscribe'),\n  getState: getChannelName(name, 'get-state'),\n  callAction: getChannelName(name, 'call-action'),\n  stateUpdated: getChannelName(name, 'state-updated')\n});\n\n// eslint-disable-next-line @typescript-eslint/ban-types\nexport type RemoteState<State, Actions extends Record<string, Function>> = (sendUpdate: (state?: State, id?: string) => void) => Promisable<{\n  getState: (id?: string) => Promisable<State>;\n  actions: Actions;\n  subscribe?: (id?: string) => undefined | (() => void);\n}>;\n"
  },
  {
    "path": "main/tray.ts",
    "content": "'use strict';\n\nimport {Tray} from 'electron';\nimport {KeyboardEvent} from 'electron/main';\nimport path from 'path';\nimport {getCogMenu} from './menus/cog';\nimport {getRecordMenu} from './menus/record';\nimport {track} from './common/analytics';\nimport {openFiles} from './utils/open-files';\nimport {windowManager} from './windows/manager';\nimport {pauseRecording, resumeRecording, stopRecording} from './aperture';\n\nlet tray: Tray;\nlet trayAnimation: NodeJS.Timeout | undefined;\n\nconst openContextMenu = async () => {\n  tray.popUpContextMenu(await getCogMenu());\n};\n\nconst openRecordingContextMenu = async () => {\n  tray.popUpContextMenu(await getRecordMenu(false));\n};\n\nconst openPausedContextMenu = async () => {\n  tray.popUpContextMenu(await getRecordMenu(true));\n};\n\nconst openCropperWindow = () => windowManager.cropper?.open();\n\nexport const initializeTray = () => {\n  tray = new Tray(path.join(__dirname, '..', 'static', 'menubarDefaultTemplate.png'));\n  tray.on('click', openCropperWindow);\n  tray.on('right-click', openContextMenu);\n  tray.on('drop-files', (_, files) => {\n    track('editor/opened/tray');\n    openFiles(...files);\n  });\n\n  return tray;\n};\n\nexport const disableTray = () => {\n  tray.removeListener('click', openCropperWindow);\n  tray.removeListener('right-click', openContextMenu);\n};\n\nexport const resetTray = () => {\n  if (trayAnimation) {\n    clearTimeout(trayAnimation);\n  }\n\n  tray.removeAllListeners('click');\n  tray.removeAllListeners('right-click');\n\n  tray.setImage(path.join(__dirname, '..', 'static', 'menubarDefaultTemplate.png'));\n  tray.on('click', openCropperWindow);\n  tray.on('right-click', openContextMenu);\n};\n\nexport const setRecordingTray = () => {\n  animateIcon();\n\n  tray.removeAllListeners('right-click');\n\n  // TODO: figure out why this is marked as missing. It's defined properly in the electron.d.ts file\n  tray.once('click', onRecordingTrayClick);\n  tray.on('right-click', openRecordingContextMenu);\n};\n\nexport const setPausedTray = () => {\n  if (trayAnimation) {\n    clearTimeout(trayAnimation);\n  }\n\n  tray.removeAllListeners('right-click');\n\n  tray.setImage(path.join(__dirname, '..', 'static', 'pauseTemplate.png'));\n  tray.once('click', resumeRecording);\n  tray.on('right-click', openPausedContextMenu);\n};\n\nconst onRecordingTrayClick = (event: KeyboardEvent) => {\n  if (event.altKey) {\n    pauseRecording();\n    return;\n  }\n\n  stopRecording();\n};\n\nconst animateIcon = async () => new Promise<void>(resolve => {\n  const interval = 20;\n  let i = 0;\n\n  const next = () => {\n    trayAnimation = setTimeout(() => {\n      const number = String(i++).padStart(5, '0');\n      const filename = `loading_${number}Template.png`;\n\n      try {\n        tray.setImage(path.join(__dirname, '..', 'static', 'menubar-loading', filename));\n        next();\n      } catch {\n        trayAnimation = undefined;\n        resolve();\n      }\n    }, interval);\n  };\n\n  next();\n});\n"
  },
  {
    "path": "main/utils/ajv.ts",
    "content": "import Ajv, {Options} from 'ajv';\nimport {Schema as JSONSchema} from 'electron-store';\nimport {Except} from 'type-fest';\n\nexport type Schema<T extends {'required': boolean} = any> = Except<JSONSchema<T>, 'required'> & {\n  required?: boolean;\n  customType?: string;\n};\n\nconst hexColorValidator = () => {\n  return {\n    type: 'string',\n    pattern: /^((0x)|#)([\\dA-Fa-f]{8}|[\\dA-Fa-f]{6})$/.source\n  };\n};\n\nconst keyboardShortcutValidator = () => {\n  return {\n    type: 'string'\n  };\n};\n\n// eslint-disable-next-line @typescript-eslint/ban-types\nconst validators = new Map<string, (parentSchema: object) => object>([\n  ['hexColor', hexColorValidator],\n  ['keyboardShortcut', keyboardShortcutValidator]\n]);\n\nexport default class CustomAjv extends Ajv {\n  constructor(options: Options) {\n    super(options);\n\n    this.addKeyword('customType', {\n      // eslint-disable-next-line @typescript-eslint/ban-types\n      macro: (schema: string, parentSchema: object) => {\n        const validator = validators.get(schema);\n\n        if (!validator) {\n          throw new Error(`No custom type found for ${schema}`);\n        }\n\n        return validator(parentSchema);\n      },\n      metaSchema: {\n        type: 'string',\n        enum: [...validators.keys()]\n      }\n    });\n  }\n}\n"
  },
  {
    "path": "main/utils/deep-linking.ts",
    "content": "import {windowManager} from '../windows/manager';\n\nconst pluginPromises = new Map<string, (path: string) => void>();\n\nconst handlePluginsDeepLink = (path: string) => {\n  const [plugin, ...rest] = path.split('/');\n\n  if (pluginPromises.has(plugin)) {\n    pluginPromises.get(plugin)?.(rest.join('/'));\n    pluginPromises.delete(plugin);\n    return;\n  }\n\n  console.error(`Received link for plugin “${plugin}” but there was no registered listener.`);\n};\n\nexport const addPluginPromise = (plugin: string, resolveFunction: (path: string) => void) => {\n  pluginPromises.set(plugin, resolveFunction);\n};\n\nconst triggerPluginAction = (action: string) => (name: string) => windowManager.preferences?.open({target: {name, action}});\n\nconst routes = new Map([\n  ['plugins', handlePluginsDeepLink],\n  ['install-plugin', triggerPluginAction('install')],\n  ['configure-plugin', triggerPluginAction('configure')]\n]);\n\nexport const handleDeepLink = (url: string) => {\n  const {host, pathname} = new URL(url);\n\n  if (routes.has(host)) {\n    return routes.get(host)?.(pathname.slice(1));\n  }\n\n  console.error(`Route not recognized: ${host} (${url}).`);\n};\n"
  },
  {
    "path": "main/utils/devices.ts",
    "content": "import {hasMicrophoneAccess} from '../common/system-permissions';\nimport * as audioDevices from 'macos-audio-devices';\nimport {settings} from '../common/settings';\nimport {defaultInputDeviceId} from '../common/constants';\nimport Sentry from './sentry';\nconst aperture = require('aperture');\n\nconst {showError} = require('./errors');\n\nexport const getAudioDevices = async () => {\n  if (!hasMicrophoneAccess()) {\n    return [];\n  }\n\n  try {\n    const devices = await audioDevices.getInputDevices();\n\n    return devices.sort((a, b) => {\n      if (a.transportType === b.transportType) {\n        return a.name.localeCompare(b.name);\n      }\n\n      if (a.transportType === 'builtin') {\n        return -1;\n      }\n\n      if (b.transportType === 'builtin') {\n        return 1;\n      }\n\n      return 0;\n    }).map(device => ({id: device.uid, name: device.name}));\n  } catch (error) {\n    try {\n      const devices = await aperture.audioDevices();\n\n      if (!Array.isArray(devices)) {\n        Sentry.captureException(new Error(`devices is not an array: ${JSON.stringify(devices)}`));\n        showError(error);\n        return [];\n      }\n\n      return devices;\n    } catch (error) {\n      showError(error);\n      return [];\n    }\n  }\n};\n\nexport const getDefaultInputDevice = () => {\n  try {\n    const device = audioDevices.getDefaultInputDevice.sync();\n    return {\n      id: device.uid,\n      name: device.name\n    };\n  } catch {\n    // Running on 10.13 and don't have swift support libs. No need to report\n    return undefined;\n  }\n};\n\nexport const getSelectedInputDeviceId = () => {\n  const audioInputDeviceId = settings.get('audioInputDeviceId', defaultInputDeviceId);\n\n  if (audioInputDeviceId === defaultInputDeviceId) {\n    const device = getDefaultInputDevice();\n    return device?.id;\n  }\n\n  return audioInputDeviceId;\n};\n\nexport const initializeDevices = async () => {\n  const audioInputDeviceId = settings.get('audioInputDeviceId');\n\n  if (hasMicrophoneAccess()) {\n    const devices = await getAudioDevices();\n\n    if (!devices.some((device: any) => device.id === audioInputDeviceId)) {\n      settings.set('audioInputDeviceId', defaultInputDeviceId);\n    }\n  }\n};\n"
  },
  {
    "path": "main/utils/dock.ts",
    "content": "import {app} from 'electron';\nimport {Promisable} from 'type-fest';\n\nexport const ensureDockIsShowing = async (action: () => Promisable<void>) => {\n  const wasDockShowing = app.dock.isVisible();\n  if (!wasDockShowing) {\n    await app.dock.show();\n  }\n\n  await action();\n\n  if (!wasDockShowing) {\n    app.dock.hide();\n  }\n};\n\nexport const ensureDockIsShowingSync = (action: () => void) => {\n  const wasDockShowing = app.dock.isVisible();\n  if (!wasDockShowing) {\n    app.dock.show();\n  }\n\n  action();\n\n  if (!wasDockShowing) {\n    app.dock.hide();\n  }\n};\n"
  },
  {
    "path": "main/utils/encoding.ts",
    "content": "/* eslint-disable array-element-newline */\n\nimport path from 'path';\nimport execa from 'execa';\nimport tempy from 'tempy';\nimport {track} from '../common/analytics';\nimport ffmpegPath from './ffmpeg-path';\n\nexport const getEncoding = async (filePath: string) => {\n  try {\n    await execa(ffmpegPath, ['-i', filePath]);\n    return undefined;\n  } catch (error) {\n    return /.*: Video: (.*?) \\(.*/.exec((error as any)?.stderr)?.[1];\n  }\n};\n\n// `ffmpeg -i original.mp4 -vcodec libx264 -crf 27 -preset veryfast -c:a copy output.mp4`\nexport const convertToH264 = async (inputPath: string) => {\n  const outputPath = tempy.file({extension: path.extname(inputPath)});\n\n  track('encoding/converted/hevc');\n\n  await execa(ffmpegPath, [\n    '-i', inputPath,\n    '-vcodec', 'libx264',\n    '-crf', '27',\n    '-preset', 'veryfast',\n    '-c:a', 'copy',\n    outputPath\n  ]);\n\n  return outputPath;\n};\n"
  },
  {
    "path": "main/utils/errors.ts",
    "content": "import path from 'path';\nimport {clipboard, shell, app} from 'electron';\nimport ensureError from 'ensure-error';\nimport cleanStack from 'clean-stack';\nimport isOnline from 'is-online';\nimport {openNewGitHubIssue} from 'electron-util';\nimport got from 'got';\nimport delay from 'delay';\nimport macosRelease from './macos-release';\n\nimport {windowManager} from '../windows/manager';\nimport Sentry, {isSentryEnabled} from './sentry';\nimport {InstalledPlugin} from '../plugins/plugin';\n\nconst MAX_RETRIES = 10;\n\nconst ERRORS_TO_IGNORE = [\n  /net::ERR_CONNECTION_TIMED_OUT/,\n  /net::ERR_NETWORK_IO_SUSPENDED/,\n  /net::ERR_CONNECTION_CLOSED/\n];\n\nconst shouldIgnoreError = (errorText: string) => ERRORS_TO_IGNORE.some(regex => regex.test(errorText));\n\ntype SentryIssue = {\n  issueId: string;\n  shortId: string;\n  permalink: string;\n  ghUrl: string;\n} | {\n  issueId: string;\n  shortId: string;\n  permalink: string;\n  ghIssueTemplate: string;\n} | {\n  error: string;\n};\n\nconst getSentryIssue = async (eventId: string, tries = 0): Promise<SentryIssue | undefined | void> => {\n  if (tries > MAX_RETRIES) {\n    return;\n  }\n\n  try {\n    // This endpoint will poll the Sentry API with the event ID until it gets an issue ID (~8 seconds).\n    // It will then filter through GitHub issues to try and find an issue matching that issue ID.\n    // It will return the issue information if it finds it or a partial template to use to create one if not.\n    // https://github.com/wulkano/kap-sentry-tracker\n    const {body} = await got.get(`https://kap-sentry-tracker.vercel.app/api/event/${eventId}`, {json: true});\n\n    if (body.pending) {\n      await delay(2000);\n      return await getSentryIssue(eventId, tries + 1);\n    }\n\n    return body;\n  } catch (error) {\n    // We are not using `showError` again here to avoid an infinite error cycle\n    console.log(error);\n  }\n};\n\nconst getPrettyStack = (error: Error) => {\n  const pluginsPath = path.join(app.getPath('userData'), 'plugins', 'node_modules');\n  return cleanStack(error.stack ?? '', {pretty: true, basePath: pluginsPath});\n};\n\nconst release = macosRelease();\n\nconst getIssueBody = (title: string, errorStack: string, sentryTemplate = '') => `\n${sentryTemplate}<!--\nThank you for helping us test Kap. Your feedback helps us make Kap better for everyone!\n-->\n\n**macOS version:**    ${release.name} (${release.version})\n**Kap version:**      ${app.getVersion()}\n\n\\`\\`\\`\n${title}\n\n${errorStack}\n\\`\\`\\`\n\n<!-- If you have additional information, enter it below. -->\n`;\n\nexport const showError = async (\n  error: Error,\n  {\n    title: customTitle,\n    plugin\n  }: {\n    title?: string;\n    plugin?: InstalledPlugin;\n  } = {}\n) => {\n  await app.whenReady();\n  const ensuredError = ensureError(error);\n  const title = customTitle ?? ensuredError.name;\n  const detail = getPrettyStack(ensuredError);\n\n  console.log(error);\n  if (shouldIgnoreError(`${title}\\n${detail}`)) {\n    return;\n  }\n\n  const mainButtons = [\n    'Don\\'t Report',\n    {\n      label: 'Copy Error',\n      action: () => {\n        clipboard.writeText(`${title}\\n${detail}`);\n      }\n    }\n  ];\n\n  // If it's a plugin error, offer to open an issue on the plugin repo (if known)\n  if (plugin) {\n    const openIssueButton = plugin.repoUrl && {\n      label: 'Open Issue',\n      action: () => {\n        openNewGitHubIssue({\n          repoUrl: plugin.repoUrl,\n          title,\n          body: getIssueBody(title, detail)\n        });\n      }\n    };\n\n    return windowManager.dialog?.open({\n      title,\n      detail,\n      cancelId: 0,\n      defaultId: openIssueButton ? 2 : 0,\n      buttons: [...mainButtons, openIssueButton].filter(Boolean)\n    });\n  }\n\n  let message;\n  const buttons: any[] = [...mainButtons];\n\n  if (isSentryEnabled && await isOnline()) {\n    const eventId = Sentry.captureException(ensuredError);\n    const sentryIssuePromise = getSentryIssue(eventId);\n\n    message = 'Reporting this issue will help us track it better and resolve it faster.';\n\n    buttons.push({\n      label: 'Collect Info and Report',\n      activeLabel: 'Collecting Info…',\n      action: async (_: unknown, updateUi: any) => {\n        const issue = await sentryIssuePromise;\n\n        if (!issue || 'error' in issue) {\n          updateUi({\n            message: 'Something went wrong while collecting the information.',\n            buttons: mainButtons\n          });\n        } else if ('ghUrl' in issue) {\n          updateUi({\n            message: 'This issue is already being tracked!',\n            buttons: [\n              ...mainButtons, {\n                label: 'View Issue',\n                action: async () => shell.openExternal(issue.ghUrl)\n              }\n            ]\n          });\n        } else {\n          updateUi({\n            buttons: [\n              ...mainButtons,\n              {\n                label: 'Open Issue',\n                action: () => {\n                  openNewGitHubIssue({\n                    user: 'wulkano',\n                    repo: 'kap',\n                    title,\n                    body: getIssueBody(title, detail, issue.ghIssueTemplate),\n                    labels: ['sentry']\n                  });\n                }\n              }\n            ]\n          });\n        }\n      }\n    });\n  }\n\n  return windowManager.dialog?.open({\n    title,\n    detail,\n    buttons,\n    message,\n    cancelId: 0,\n    defaultId: buttons.length > 2 ? 2 : 0\n  });\n};\n\nexport const setupErrorHandling = () => {\n  process.on('uncaughtException', error => {\n    showError(error, {title: 'Unhandled Error'});\n  });\n\n  process.on('unhandledRejection', error => {\n    showError(ensureError(error), {title: 'Unhandled Promise Rejection'});\n  });\n};\n"
  },
  {
    "path": "main/utils/ffmpeg-path.ts",
    "content": "import ffmpeg from 'ffmpeg-static';\nimport util from 'electron-util';\n\nconst ffmpegPath = util.fixPathForAsarUnpack(ffmpeg);\n\nexport default ffmpegPath;\n"
  },
  {
    "path": "main/utils/format-time.ts",
    "content": "import moment from 'moment';\n\nconst formatTime = (time: number, options: any) => {\n  options = {\n    showMilliseconds: false,\n    ...options\n  };\n\n  const durationFormatted = options.extra ?\n    `  (${format(options.extra, options)})` :\n    '';\n\n  return `${format(time, options)}${durationFormatted}`;\n};\n\nconst format = (time: number, {showMilliseconds} = {showMilliseconds: false}) => {\n  const formatString = `${time >= 60 * 60 ? 'hh:m' : ''}m:ss${showMilliseconds ? '.SS' : ''}`;\n\n  return moment().startOf('day').millisecond(time * 1000).format(formatString);\n};\n\nexport default formatTime;\n"
  },
  {
    "path": "main/utils/formats.ts",
    "content": "import {Format} from '../common/types';\n\nconst formats = new Map([\n  [Format.gif, 'GIF'],\n  [Format.hevc, 'MP4 (H265)'],\n  [Format.mp4, 'MP4 (H264)'],\n  [Format.av1, 'MP4 (AV1)'],\n  [Format.webm, 'WebM'],\n  [Format.apng, 'APNG']\n]);\n\nexport const prettifyFormat = (format: Format): string => {\n  return formats.get(format)!;\n};\n"
  },
  {
    "path": "main/utils/fps.ts",
    "content": "import execa from 'execa';\nimport ffmpegPath from './ffmpeg-path';\n\nconst getFps = async (filePath: string) => {\n  try {\n    await execa(ffmpegPath, ['-i', filePath]);\n    return undefined;\n  } catch (error) {\n    return /.*, (.*) fp.*/.exec((error as any)?.stderr)?.[1];\n  }\n};\n\nexport default getFps;\n"
  },
  {
    "path": "main/utils/image-preview.ts",
    "content": "/* eslint-disable array-element-newline */\n\nimport {BrowserWindow, dialog} from 'electron';\nimport execa from 'execa';\nimport tempy from 'tempy';\nimport {promisify} from 'util';\nimport type {Video} from '../video';\nimport {generateTimestampedName} from './timestamped-name';\nimport ffmpegPath from './ffmpeg-path';\n\nconst base64Img = require('base64-img');\n\nconst getBase64 = promisify(base64Img.base64);\n\nexport const generatePreviewImage = async (filePath: string): Promise<{path: string; data: string} | undefined> => {\n  const previewPath = tempy.file({extension: '.jpg'});\n\n  try {\n    await execa(ffmpegPath, [\n      '-ss', '0',\n      '-i', filePath,\n      '-t', '1',\n      '-vframes', '1',\n      '-f', 'image2',\n      previewPath\n    ]);\n  } catch {\n    return;\n  }\n\n  try {\n    return {\n      path: previewPath,\n      data: await getBase64(previewPath)\n    };\n  } catch {\n    return {\n      path: previewPath,\n      data: ''\n    };\n  }\n};\n\nexport const saveSnapshot = async (video: Video, time: number) => {\n  const {filePath: outputPath} = await dialog.showSaveDialog(BrowserWindow.getFocusedWindow()!, {\n    defaultPath: generateTimestampedName('Snapshot', '.jpg')\n  });\n\n  if (outputPath) {\n    await execa(ffmpegPath, [\n      '-i', video.filePath,\n      '-ss', time.toString(),\n      '-vframes', '1',\n      outputPath\n    ]);\n  }\n};\n"
  },
  {
    "path": "main/utils/macos-release.ts",
    "content": "// Vendored: https://github.com/sindresorhus/macos-release\n\n'use strict';\nconst os = require('os');\n\nconst nameMap = {\n  22: ['Ventura', '13'],\n  21: ['Monterey', '12'],\n  20: ['Big Sur', '11'],\n  19: ['Catalina', '10.15'],\n  18: ['Mojave', '10.14'],\n  17: ['High Sierra', '10.13'],\n  16: ['Sierra', '10.12'],\n  15: ['El Capitan', '10.11'],\n  14: ['Yosemite', '10.10'],\n  13: ['Mavericks', '10.9'],\n  12: ['Mountain Lion', '10.8'],\n  11: ['Lion', '10.7'],\n  10: ['Snow Leopard', '10.6'],\n  9: ['Leopard', '10.5'],\n  8: ['Tiger', '10.4'],\n  7: ['Panther', '10.3'],\n  6: ['Jaguar', '10.2'],\n  5: ['Puma', '10.1']\n} as const;\n\nexport default function macosRelease(release?: string) {\n  const releaseCleaned = (release ?? os.release()).split('.')[0] as keyof typeof nameMap;\n  const [name, version] = nameMap[releaseCleaned] ?? ['Unknown', ''];\n\n  return {\n    name,\n    version\n  };\n}\n"
  },
  {
    "path": "main/utils/notifications.ts",
    "content": "import {Notification, NotificationConstructorOptions, NotificationAction, app} from 'electron';\n\n// Need to persist the notifications, otherwise it is garbage collected and the actions don't trigger\n// https://github.com/electron/electron/issues/12690\nconst notifications = new Set<Notification>();\n\ninterface Action extends NotificationAction {\n  action?: () => void | Promise<void>;\n}\n\ninterface NotificationOptions extends NotificationConstructorOptions {\n  actions?: Action[];\n  click?: () => void | Promise<void>;\n  show?: boolean;\n}\n\ntype NotificationPromise = Promise<void> & {\n  show: () => void;\n  close: () => void;\n};\n\nexport const notify = (options: NotificationOptions): NotificationPromise => {\n  const notification = new Notification(options);\n\n  notifications.add(notification);\n\n  const promise = new Promise(resolve => {\n    if (options.click && typeof options.click === 'function') {\n      notification.on('click', () => {\n        resolve(options.click?.());\n      });\n    }\n\n    if (options.actions && options.actions.length > 0) {\n      notification.on('action', (_, index) => {\n        const button = options.actions?.[index];\n\n        if (button?.action && typeof button?.action === 'function') {\n          resolve(button?.action?.());\n        } else {\n          resolve(index);\n        }\n      });\n    }\n\n    notification.on('close', () => {\n      resolve(undefined);\n    });\n  });\n\n  promise.then(() => {\n    notifications.delete(notification);\n  });\n\n  (promise as NotificationPromise).show = () => {\n    notification.show();\n  };\n\n  (promise as NotificationPromise).close = () => {\n    notification.close();\n  };\n\n  if (options.show ?? true) {\n    notification.show();\n  }\n\n  return promise as NotificationPromise;\n};\n\nnotify.simple = (text: string) => notify({title: app.name, body: text});\n"
  },
  {
    "path": "main/utils/open-files.ts",
    "content": "'use strict';\nimport path from 'path';\nimport {supportedVideoExtensions} from '../common/constants';\nimport {Video} from '../video';\n\nconst fileExtensions = supportedVideoExtensions.map(ext => `.${ext}`);\n\nexport const openFiles = async (...filePaths: string[]) => {\n  return Promise.all(\n    filePaths\n      .filter(filePath => fileExtensions.includes(path.extname(filePath).toLowerCase()))\n      .map(async filePath => {\n        return Video.getOrCreate({\n          filePath\n        }).openEditorWindow();\n      })\n  );\n};\n\n"
  },
  {
    "path": "main/utils/protocol.ts",
    "content": "import {protocol} from 'electron';\n\nexport const setupProtocol = () => {\n  // Fix protocol issue in order to support loading editor previews\n  // https://github.com/electron/electron/issues/23757#issuecomment-640146333\n  protocol.registerFileProtocol('file', (request, callback) => {\n    const pathname = decodeURI(request.url.replace('file:///', ''));\n    callback(pathname);\n  });\n};\n"
  },
  {
    "path": "main/utils/routes.ts",
    "content": "import {app, BrowserWindow} from 'electron';\nimport {is} from 'electron-util';\n\nexport const loadRoute = (window: BrowserWindow, routeName: string, {openDevTools}: {openDevTools?: boolean} = {}) => {\n  if (is.development) {\n    window.loadURL(`http://localhost:8000/${routeName}`);\n    window.webContents.openDevTools({mode: 'detach'});\n  } else {\n    window.loadFile(`${app.getAppPath()}/renderer/out/${routeName}.html`);\n    if (openDevTools) {\n      window.webContents.openDevTools({mode: 'detach'});\n    }\n  }\n};\n"
  },
  {
    "path": "main/utils/sentry.ts",
    "content": "'use strict';\n\nimport {app} from 'electron';\nimport {is} from 'electron-util';\nimport * as Sentry from '@sentry/electron';\nimport {settings} from '../common/settings';\n\nconst SENTRY_PUBLIC_DSN = 'https://2dffdbd619f34418817f4db3309299ce@sentry.io/255536';\n\nexport const isSentryEnabled = !is.development && settings.get('allowAnalytics');\n\nif (isSentryEnabled) {\n  const release = `${app.name}@${app.getVersion()}`.toLowerCase();\n  Sentry.init({\n    dsn: SENTRY_PUBLIC_DSN,\n    release\n  });\n}\n\nexport default Sentry;\n"
  },
  {
    "path": "main/utils/shortcut-to-accelerator.ts",
    "content": "\nexport const shortcutToAccelerator = (shortcut: any) => {\n  const {metaKey, altKey, ctrlKey, shiftKey, character} = shortcut;\n  if (!character) {\n    throw new Error(`shortcut needs character ${JSON.stringify(shortcut)}`);\n  }\n\n  const keys = [\n    metaKey && 'Command',\n    altKey && 'Option',\n    ctrlKey && 'Control',\n    shiftKey && 'Shift',\n    character\n  ].filter(Boolean);\n  return keys.join('+');\n};\n"
  },
  {
    "path": "main/utils/timestamped-name.ts",
    "content": "import moment from 'moment';\n\nexport const generateTimestampedName = (title = 'Kapture', extension = '') => `${title} ${moment().format('YYYY-MM-DD')} at ${moment().format('HH.mm.ss')}${extension}`;\n"
  },
  {
    "path": "main/utils/track-duration.ts",
    "content": "// TODO: Add interface to aperture-node for getting recording duration instead of using this https://github.com/wulkano/aperture-node/issues/29\nlet overallDuration = 0;\nlet currentDurationStart = 0;\n\nexport const getOverallDuration = (): number => overallDuration;\n\nexport const getCurrentDurationStart = (): number => currentDurationStart;\n\nexport const setOverallDuration = (duration: number): void => {\n  overallDuration = duration;\n};\n\nexport const setCurrentDurationStart = (duration: number): void => {\n  currentDurationStart = duration;\n};\n"
  },
  {
    "path": "main/utils/windows.ts",
    "content": "import {Menu, MenuItem, nativeImage} from 'electron';\nimport Store from 'electron-store';\nimport {windowManager} from '../windows/manager';\n\nconst {getWindows, activateWindow} = require('mac-windows');\nconst {getAppIconListByPid} = require('node-mac-app-icon');\n\nexport interface MacWindow {\n  pid: number;\n  ownerName: string;\n  name: string;\n  width: number;\n  height: number;\n  x: number;\n  y: number;\n  number: number;\n}\n\nconst APP_BLACKLIST = [\n  'Kap',\n  'Kap Beta'\n];\n\nconst store = new Store<{\n  appUsageHistory: Record<number, {\n    count: number;\n    lastUsed: number;\n  } | undefined>;\n}>({\n  name: 'usage-history'\n});\n\nconst usageHistory = store.get('appUsageHistory', {});\n\nconst isValidApp = ({ownerName}: MacWindow) => !APP_BLACKLIST.includes(ownerName);\n\nconst getWindowList = async () => {\n  const windows = await getWindows() as MacWindow[];\n  const images = await getAppIconListByPid(windows.map(win => win.pid), {\n    size: 16,\n    failOnError: false\n  }) as Array<{\n    pid: number;\n    icon: Buffer;\n  }>;\n\n  let maxLastUsed = 0;\n\n  return windows.filter(window => isValidApp(window)).map(win => {\n    const iconImage = images.find(img => img.pid === win.pid);\n    const icon = iconImage?.icon ? nativeImage.createFromBuffer(iconImage.icon) : undefined;\n\n    const window = {\n      ...win,\n      icon2x: icon,\n      icon: icon?.resize({width: 16, height: 16}),\n      count: 0,\n      lastUsed: 0,\n      ...usageHistory[win.pid]\n    };\n\n    maxLastUsed = Math.max(maxLastUsed, window.lastUsed);\n    return window;\n  }).sort((a, b) => {\n    if (a.lastUsed === maxLastUsed) {\n      return -1;\n    }\n\n    if (b.lastUsed === maxLastUsed) {\n      return 1;\n    }\n\n    return b.count - a.count;\n  });\n};\n\nexport const buildWindowsMenu = async (selected: string) => {\n  const menu = new Menu();\n  const windows = await getWindowList();\n\n  for (const win of windows) {\n    menu.append(\n      new MenuItem({\n        label: win.ownerName,\n        icon: win.icon,\n        type: 'checkbox',\n        checked: win.ownerName === selected,\n        click: () => {\n          activateApp(win);\n        }\n      })\n    );\n  }\n\n  return menu;\n};\n\nconst updateAppUsageHistory = (app: MacWindow) => {\n  const {count = 0} = usageHistory[app.pid] ?? {};\n\n  usageHistory[app.pid] = {\n    count: count + 1,\n    lastUsed: Date.now()\n  };\n\n  store.set('appUsageHistory', usageHistory);\n};\n\nexport const activateApp = (window: MacWindow) => {\n  updateAppUsageHistory(window);\n  windowManager.cropper?.selectApp(window, activateWindow);\n};\n"
  },
  {
    "path": "main/video.ts",
    "content": "import path from 'path';\nimport getFps from './utils/fps';\nimport {getEncoding, convertToH264} from './utils/encoding';\nimport {nativeImage, NativeImage, screen} from 'electron';\nimport {ApertureOptions, Encoding} from './common/types';\nimport {generateTimestampedName} from './utils/timestamped-name';\nimport fs from 'fs';\nimport {generatePreviewImage} from './utils/image-preview';\nimport {windowManager} from './windows/manager';\n\ninterface VideoOptions {\n  filePath: string;\n  title?: string;\n  fps?: number;\n  encoding?: Encoding;\n  previewPath?: string;\n  pixelDensity?: number;\n  isNewRecording?: boolean;\n}\n\nexport class Video {\n  static all = new Map<string, Video>();\n\n  filePath: string;\n  title: string;\n  fps?: number;\n  encoding?: Encoding;\n  pixelDensity: number;\n  previewPath?: string;\n  dragIcon?: NativeImage;\n  isNewRecording = false;\n  isReady = false;\n  previewImage?: {path: string; data: string};\n\n  private readonly readyPromise: Promise<void>;\n  private readonly previewReadyPromise: Promise<string | undefined>;\n\n  constructor(options: VideoOptions) {\n    this.filePath = options.filePath;\n    this.title = options.title ?? path.parse(this.filePath).name;\n    this.fps = options.fps;\n    this.encoding = options.encoding;\n    this.pixelDensity = options.pixelDensity ?? 1;\n    this.isNewRecording = options.isNewRecording ?? false;\n    this.previewPath = options.previewPath;\n\n    Video.all.set(this.filePath, this);\n\n    this.readyPromise = this.collectInfo();\n    this.previewReadyPromise = this.readyPromise.then(async () => this.getPreviewPath());\n  }\n\n  static fromId(id: string) {\n    return this.all.get(id);\n  }\n\n  static getOrCreate(options: VideoOptions) {\n    return Video.fromId(options.filePath) ?? new Video(options);\n  }\n\n  async getFps() {\n    if (!this.fps) {\n      this.fps = Math.round(Number.parseFloat((await getFps(this.filePath)) ?? '0'));\n    }\n\n    return this.fps;\n  }\n\n  async exists() {\n    try {\n      await fs.promises.access(this.filePath, fs.constants.F_OK);\n      return true;\n    } catch {\n      return false;\n    }\n  }\n\n  async getEncoding() {\n    if (!this.encoding) {\n      this.encoding = (await getEncoding(this.filePath)) as Encoding;\n    }\n\n    return this.encoding;\n  }\n\n  async getPreviewPath() {\n    if (!await this.exists()) {\n      return;\n    }\n\n    if (!this.previewPath) {\n      if (this.encoding === 'h264') {\n        this.previewPath = this.filePath;\n      } else {\n        this.previewPath = await convertToH264(this.filePath);\n      }\n    }\n\n    return this.previewPath;\n  }\n\n  async getDragIcon({width, height}: {width: number; height: number}) {\n    const previewImagePath = (await this.generatePreviewImage())?.path;\n\n    if (previewImagePath) {\n      const resizeOptions = width > height ? {width: 64} : {height: 64};\n      return nativeImage.createFromPath(previewImagePath).resize(resizeOptions);\n    }\n\n    return nativeImage.createEmpty();\n  }\n\n  async generatePreviewImage() {\n    if (!this.previewImage) {\n      this.previewImage = await generatePreviewImage(this.filePath);\n    }\n\n    return this.previewImage;\n  }\n\n  async whenReady() {\n    return this.readyPromise;\n  }\n\n  async whenPreviewReady() {\n    return this.previewReadyPromise;\n  }\n\n  async openEditorWindow() {\n    return windowManager.editor?.open(this);\n  }\n\n  private async collectInfo() {\n    if (!await this.exists()) {\n      return;\n    }\n\n    await Promise.all([\n      this.getFps(),\n      this.getEncoding()\n    ]);\n\n    this.isReady = true;\n  }\n}\n\nexport class Recording extends Video {\n  apertureOptions: ApertureOptions;\n\n  constructor(options: VideoOptions & {apertureOptions: ApertureOptions}) {\n    const displays = screen.getAllDisplays();\n    const pixelDensity = displays.find(display => display.id === options.apertureOptions.screenId)?.scaleFactor;\n\n    super({\n      filePath: options.filePath,\n      title: options.title ?? generateTimestampedName(),\n      fps: options.apertureOptions.fps,\n      encoding: options.apertureOptions.videoCodec ?? Encoding.h264,\n      pixelDensity\n    });\n\n    this.apertureOptions = options.apertureOptions;\n    this.isNewRecording = true;\n  }\n}\n"
  },
  {
    "path": "main/windows/config.ts",
    "content": "'use strict';\n\nimport {BrowserWindow} from 'electron';\nimport {ipcMain as ipc} from 'electron-better-ipc';\nimport pEvent from 'p-event';\n\nimport {loadRoute} from '../utils/routes';\nimport {windowManager} from './manager';\n\nconst openConfigWindow = async (pluginName: string) => {\n  const prefsWindow = await windowManager.preferences?.open();\n  const configWindow = new BrowserWindow({\n    width: 320,\n    height: 436,\n    resizable: false,\n    movable: false,\n    minimizable: false,\n    maximizable: false,\n    fullscreenable: false,\n    titleBarStyle: 'hiddenInset',\n    show: false,\n    parent: prefsWindow,\n    modal: true,\n    webPreferences: {\n      nodeIntegration: true,\n      enableRemoteModule: true,\n      contextIsolation: false\n    }\n  });\n\n  loadRoute(configWindow, 'config');\n\n  configWindow.webContents.on('did-finish-load', async () => {\n    await ipc.callRenderer(configWindow, 'plugin', pluginName);\n    configWindow.show();\n  });\n\n  await pEvent(configWindow, 'closed');\n};\n\nconst openEditorConfigWindow = async (pluginName: string, serviceTitle: string, editorWindow: BrowserWindow) => {\n  const configWindow = new BrowserWindow({\n    width: 480,\n    height: 420,\n    resizable: false,\n    movable: false,\n    minimizable: false,\n    maximizable: false,\n    fullscreenable: false,\n    titleBarStyle: 'hiddenInset',\n    show: false,\n    parent: editorWindow,\n    modal: true,\n    webPreferences: {\n      nodeIntegration: true,\n      enableRemoteModule: true,\n      contextIsolation: false\n    }\n  });\n\n  loadRoute(configWindow, 'config');\n\n  configWindow.webContents.on('did-finish-load', async () => {\n    await ipc.callRenderer(configWindow, 'edit-service', {pluginName, serviceTitle});\n    configWindow.show();\n  });\n\n  await pEvent(configWindow, 'closed');\n};\n\nipc.answerRenderer('open-edit-config', async ({pluginName, serviceTitle}, window) => {\n  return openEditorConfigWindow(pluginName, serviceTitle, window);\n});\n\nwindowManager.setConfig({\n  open: openConfigWindow\n});\n"
  },
  {
    "path": "main/windows/cropper.ts",
    "content": "\nimport {windowManager} from './manager';\nimport {BrowserWindow, systemPreferences, dialog, screen, Display, app} from 'electron';\nimport delay from 'delay';\n\nimport {settings} from '../common/settings';\nimport {hasMicrophoneAccess, ensureMicrophonePermissions, openSystemPreferences, ensureScreenCapturePermissions} from '../common/system-permissions';\nimport {loadRoute} from '../utils/routes';\nimport {MacWindow} from '../utils/windows';\n\nconst croppers = new Map<number, BrowserWindow>();\nlet notificationId: number | undefined;\nlet isOpen = false;\n\nconst closeAllCroppers = () => {\n  screen.removeAllListeners('display-removed');\n  screen.removeAllListeners('display-added');\n\n  for (const [id, cropper] of croppers) {\n    cropper.destroy();\n    croppers.delete(id);\n  }\n\n  isOpen = false;\n\n  if (notificationId !== undefined) {\n    systemPreferences.unsubscribeWorkspaceNotification(notificationId);\n    notificationId = undefined;\n  }\n};\n\nconst openCropper = (display: Display, activeDisplayId?: number) => {\n  const {id, bounds} = display;\n  const {x, y, width, height} = bounds;\n\n  const cropper = new BrowserWindow({\n    x,\n    y,\n    width,\n    height,\n    hasShadow: false,\n    enableLargerThanScreen: true,\n    resizable: false,\n    movable: false,\n    frame: false,\n    transparent: true,\n    show: false,\n    webPreferences: {\n      nodeIntegration: true,\n      enableRemoteModule: true,\n      contextIsolation: false\n    }\n  });\n\n  loadRoute(cropper, 'cropper');\n\n  cropper.setAlwaysOnTop(true, 'screen-saver', 1);\n\n  cropper.webContents.on('did-finish-load', () => {\n    const isActive = activeDisplayId === id;\n    const displayInfo = {\n      isActive,\n      id,\n      x,\n      y,\n      width,\n      height\n    };\n\n    if (isActive) {\n      const savedCropper = settings.get('cropper', {});\n      // @ts-expect-error\n      if (savedCropper.displayId === id) {\n        // @ts-expect-error\n        displayInfo.cropper = savedCropper;\n      }\n    }\n\n    cropper.webContents.send('display', displayInfo);\n  });\n\n  cropper.on('closed', closeAllCroppers);\n  croppers.set(id, cropper);\n  return cropper;\n};\n\nconst openCropperWindow = async () => {\n  closeAllCroppers();\n  if (windowManager.editor?.areAnyBlocking()) {\n    return;\n  }\n\n  if (!ensureScreenCapturePermissions()) {\n    return;\n  }\n\n  const recordAudio = settings.get('recordAudio');\n\n  if (recordAudio && !hasMicrophoneAccess()) {\n    const granted = await ensureMicrophonePermissions(async () => {\n      const {response} = await dialog.showMessageBox({\n        type: 'warning',\n        buttons: ['Open System Preferences', 'Continue'],\n        defaultId: 1,\n        message: 'Kap cannot access the microphone.',\n        detail: 'Audio recording is enabled but Kap does not have access to the microphone. Continue without audio or grant Kap access to the microphone the System Preferences.',\n        cancelId: 2\n      });\n\n      if (response === 0) {\n        openSystemPreferences('Privacy_Microphone');\n        return false;\n      }\n\n      if (response === 1) {\n        settings.set('recordAudio', false);\n        return true;\n      }\n\n      return false;\n    });\n\n    if (!granted) {\n      return;\n    }\n  }\n\n  isOpen = true;\n\n  const displays = screen.getAllDisplays();\n  const activeDisplayId = screen.getDisplayNearestPoint(screen.getCursorScreenPoint()).id;\n\n  for (const display of displays) {\n    openCropper(display, activeDisplayId);\n  }\n\n  for (const cropper of croppers.values()) {\n    cropper.showInactive();\n  }\n\n  croppers.get(activeDisplayId)?.focus();\n\n  // Electron typing issue, this should be marked as returning a number\n  notificationId = (systemPreferences as any).subscribeWorkspaceNotification('NSWorkspaceActiveSpaceDidChangeNotification', () => {\n    closeAllCroppers();\n  });\n\n  screen.on('display-removed', (_, oldDisplay) => {\n    const {id} = oldDisplay;\n    const cropper = croppers.get(id);\n\n    if (!cropper) {\n      return;\n    }\n\n    const wasFocused = cropper.isFocused();\n\n    cropper.removeAllListeners('closed');\n    cropper.destroy();\n    croppers.delete(id);\n\n    if (wasFocused) {\n      const activeDisplayId = screen.getDisplayNearestPoint(screen.getCursorScreenPoint()).id;\n      if (croppers.has(activeDisplayId)) {\n        croppers.get(activeDisplayId)?.focus();\n      }\n    }\n  });\n\n  screen.on('display-added', (_, newDisplay) => {\n    const cropper = openCropper(newDisplay);\n    cropper.showInactive();\n  });\n};\n\nconst preventDefault = (event: any) => event.preventDefault();\n\nconst selectApp = async (window: MacWindow, activateWindow: (ownerName: string) => Promise<void>) => {\n  for (const cropper of croppers.values()) {\n    cropper.prependListener('blur', preventDefault);\n  }\n\n  await activateWindow(window.ownerName);\n\n  const {x, y, width, height, ownerName} = window;\n\n  const display = screen.getDisplayMatching({x, y, width, height});\n  const {id, bounds: {x: screenX, y: screenY}} = display;\n\n  // For some reason this happened a bit too early without the timeout\n  await delay(300);\n\n  for (const cropper of croppers.values()) {\n    cropper.removeListener('blur', preventDefault);\n    cropper.webContents.send('blur');\n  }\n\n  croppers.get(id)?.focus();\n\n  croppers.get(id)?.webContents.send('select-app', {\n    ownerName,\n    x: x - screenX,\n    y: y - screenY,\n    width,\n    height\n  });\n};\n\nconst disableCroppers = () => {\n  if (notificationId !== undefined) {\n    systemPreferences.unsubscribeWorkspaceNotification(notificationId);\n    notificationId = undefined;\n  }\n\n  for (const cropper of croppers.values()) {\n    cropper.removeAllListeners('blur');\n    cropper.setIgnoreMouseEvents(true);\n    cropper.setVisibleOnAllWorkspaces(true);\n  }\n};\n\nconst setRecordingCroppers = () => {\n  for (const cropper of croppers.values()) {\n    cropper.webContents.send('start-recording');\n  }\n};\n\nconst isCropperOpen = () => isOpen;\n\napp.on('before-quit', closeAllCroppers);\n\napp.on('browser-window-created', () => {\n  if (!isCropperOpen()) {\n    app.dock.show();\n  }\n});\n\nwindowManager.setCropper({\n  open: openCropperWindow,\n  close: closeAllCroppers,\n  selectApp,\n  setRecording: setRecordingCroppers,\n  isOpen: isCropperOpen,\n  disable: disableCroppers\n});\n"
  },
  {
    "path": "main/windows/dialog.ts",
    "content": "'use strict';\n\nimport {BrowserWindow, Rectangle} from 'electron';\nimport {ipcMain as ipc} from 'electron-better-ipc';\nimport {loadRoute} from '../utils/routes';\nimport {windowManager} from './manager';\n\nconst DIALOG_MIN_WIDTH = 420;\nconst DIALOG_MIN_HEIGHT = 150;\n\nexport type DialogOptions = any;\n\nconst showDialog = async (options: DialogOptions) => new Promise<number | void>(resolve => {\n  const dialogWindow = new BrowserWindow({\n    width: 1,\n    height: 1,\n    resizable: false,\n    minimizable: false,\n    maximizable: false,\n    fullscreenable: false,\n    vibrancy: 'window',\n    show: false,\n    alwaysOnTop: true,\n    center: true,\n    title: '',\n    useContentSize: true,\n    webPreferences: {\n      nodeIntegration: true,\n      enableRemoteModule: true,\n      contextIsolation: false\n    }\n  });\n\n  loadRoute(dialogWindow, 'dialog');\n\n  let buttons: any[];\n  let wasActionTaken;\n\n  const updateUi = async (newOptions: DialogOptions) => {\n    wasActionTaken = true;\n    buttons = newOptions.buttons.map((button: any) => {\n      if (typeof button === 'string') {\n        return {label: button};\n      }\n\n      return button;\n    });\n\n    const cancelButton = buttons.findIndex(({label}) => label === 'Cancel');\n\n    const {width, height} = await ipc.callRenderer<any, Rectangle>(dialogWindow, 'data', {\n      cancelId: cancelButton > 0 ? cancelButton : undefined,\n      ...options,\n      ...newOptions,\n      buttons: buttons.map(({label, activeLabel}) => ({label, activeLabel})),\n      id: dialogWindow.id\n    });\n\n    const bounds = dialogWindow.getBounds();\n    const titleBarHeight = dialogWindow.getSize()[1] - dialogWindow.getContentSize()[1];\n\n    dialogWindow.setBounds({\n      width: Math.max(width, bounds.width, DIALOG_MIN_WIDTH),\n      height: Math.max(height + titleBarHeight, bounds.height, DIALOG_MIN_HEIGHT)\n    });\n  };\n\n  const unsubscribe = ipc.answerRenderer(`dialog-action-${dialogWindow.id}`, async (index: number) => {\n    if (buttons[index]) {\n      if (buttons[index].action) {\n        wasActionTaken = false;\n        await buttons[index].action(cleanup, updateUi);\n\n        if (!wasActionTaken) {\n          cleanup(index);\n        }\n      } else {\n        cleanup(index);\n      }\n    } else {\n      cleanup();\n    }\n  });\n\n  const cleanup = (value?: number) => {\n    wasActionTaken = true;\n    unsubscribe();\n    dialogWindow.close();\n    resolve(value);\n  };\n\n  dialogWindow.webContents.on('did-finish-load', async () => {\n    await updateUi(options);\n    dialogWindow.show();\n  });\n});\n\nwindowManager.setDialog({\n  open: showDialog\n});\n"
  },
  {
    "path": "main/windows/editor.ts",
    "content": "import {EditorWindowState} from '../common/types';\nimport type {Video} from '../video';\nimport KapWindow from './kap-window';\nimport {MenuItemId} from '../menus/utils';\nimport {BrowserWindow, dialog} from 'electron';\nimport {is} from 'electron-util';\nimport fs from 'fs';\nimport {saveSnapshot} from '../utils/image-preview';\nimport {windowManager} from './manager';\n\nconst pify = require('pify');\n\nconst OPTIONS_BAR_HEIGHT = 48;\nconst VIDEO_ASPECT = 9 / 16;\nconst MIN_VIDEO_WIDTH = 900;\nconst MIN_VIDEO_HEIGHT = MIN_VIDEO_WIDTH * VIDEO_ASPECT;\nconst MIN_WINDOW_HEIGHT = MIN_VIDEO_HEIGHT + OPTIONS_BAR_HEIGHT;\n\nconst editors = new Map();\nconst editorsWithNotSavedDialogs = new Map();\n\nconst open = async (video: Video) => {\n  if (editors.has(video.filePath)) {\n    editors.get(video.filePath).show();\n    return;\n  }\n\n  // TODO: Make this smarter so the editor can show with a spinner while the preview is generated for longer preview conversions (like ProRes)\n  await video.whenPreviewReady();\n\n  const editorKapWindow = new KapWindow<EditorWindowState>({\n    title: video.title,\n    // TODO: Return those to the original values when we are able to resize below min size\n    // Upstream issue: https://github.com/electron/electron/issues/27025\n    // minWidth: MIN_VIDEO_WIDTH,\n    // minHeight: MIN_WINDOW_HEIGHT,\n    minWidth: 360,\n    minHeight: 392,\n    width: MIN_VIDEO_WIDTH,\n    height: MIN_WINDOW_HEIGHT,\n    backgroundColor: '#222222',\n    webPreferences: {\n      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.\n    },\n    frame: false,\n    transparent: true,\n    vibrancy: 'window',\n    route: 'editor',\n    initialState: {\n      previewFilePath: video.previewPath!,\n      filePath: video.filePath,\n      fps: video.fps!,\n      title: video.title\n    },\n    menu: defaultMenu => {\n      if (!video.isNewRecording) {\n        return;\n      }\n\n      const fileMenu = defaultMenu.find(item => item.id === MenuItemId.file);\n\n      if (fileMenu) {\n        const submenu = fileMenu.submenu as Electron.MenuItemConstructorOptions[];\n\n        const index = submenu.findIndex(item => item.id === MenuItemId.openVideo);\n\n        if (index > -1) {\n          submenu.splice(index + 1, 0, {\n            type: 'separator'\n          }, {\n            label: 'Save Original…',\n            id: MenuItemId.saveOriginal,\n            accelerator: 'Command+S',\n            click: async () => saveOriginal(video)\n          });\n        }\n      }\n    }\n  });\n\n  const editorWindow = editorKapWindow.browserWindow;\n\n  editors.set(video.filePath, editorWindow);\n\n  if (video.isNewRecording) {\n    editorWindow.setDocumentEdited(true);\n    editorWindow.on('close', (event: any) => {\n      editorsWithNotSavedDialogs.set(video.filePath, true);\n      const buttonIndex = dialog.showMessageBoxSync(editorWindow, {\n        type: 'question',\n        buttons: [\n          'Discard',\n          'Cancel'\n        ],\n        defaultId: 0,\n        cancelId: 1,\n        message: 'Are you sure that you want to discard this recording?',\n        detail: 'You will no longer be able to edit and export the original recording.'\n      });\n\n      if (buttonIndex === 1) {\n        event.preventDefault();\n      }\n\n      editorsWithNotSavedDialogs.delete(video.filePath);\n    });\n  }\n\n  editorWindow.on('closed', () => {\n    editors.delete(video.filePath);\n  });\n\n  editorWindow.on('blur', () => {\n    editorKapWindow.callRenderer('blur');\n  });\n\n  editorWindow.on('focus', () => {\n    editorKapWindow.callRenderer('focus');\n  });\n\n  editorKapWindow.answerRenderer('save-snapshot', (time: number) => {\n    saveSnapshot(video, time);\n  });\n};\n\nconst saveOriginal = async (video: Video) => {\n  const {filePath} = await dialog.showSaveDialog(BrowserWindow.getFocusedWindow()!, {\n    defaultPath: `${video.title}.mp4`\n  });\n\n  if (filePath) {\n    await pify(fs.copyFile)(video.filePath, filePath, fs.constants.COPYFILE_FICLONE);\n  }\n};\n\nconst areAnyBlocking = () => {\n  if (editorsWithNotSavedDialogs.size > 0) {\n    const [path] = editorsWithNotSavedDialogs.keys();\n    editors.get(path).focus();\n    return true;\n  }\n\n  return false;\n};\n\nwindowManager.setEditor({\n  open,\n  areAnyBlocking\n});\n"
  },
  {
    "path": "main/windows/exports.ts",
    "content": "import KapWindow from './kap-window';\nimport {windowManager} from './manager';\n\nlet exportsKapWindow: KapWindow | undefined;\n\nconst openExportsWindow = async () => {\n  if (exportsKapWindow) {\n    exportsKapWindow.browserWindow.focus();\n  } else {\n    exportsKapWindow = new KapWindow({\n      title: 'Exports',\n      width: 320,\n      height: 360,\n      resizable: false,\n      maximizable: false,\n      fullscreenable: false,\n      titleBarStyle: 'hiddenInset',\n      frame: false,\n      transparent: true,\n      vibrancy: 'window',\n      webPreferences: {\n        nodeIntegration: true,\n        contextIsolation: false\n      },\n      route: 'exports'\n    });\n\n    const exportsWindow = exportsKapWindow.browserWindow;\n\n    const titleBarHeight = 37;\n    exportsWindow.setSheetOffset(titleBarHeight);\n\n    exportsWindow.on('close', () => {\n      exportsKapWindow = undefined;\n    });\n\n    await exportsKapWindow.whenReady();\n  }\n\n  return exportsKapWindow.browserWindow;\n};\n\nconst getExportsWindow = () => exportsKapWindow?.browserWindow;\n\nwindowManager.setExports({\n  open: openExportsWindow,\n  get: getExportsWindow\n});\n"
  },
  {
    "path": "main/windows/kap-window.ts",
    "content": "import electron, {app, BrowserWindow, Menu} from 'electron';\nimport {ipcMain as ipc} from 'electron-better-ipc';\nimport pEvent from 'p-event';\nimport {customApplicationMenu, defaultApplicationMenu, MenuModifier} from '../menus/application';\nimport {loadRoute} from '../utils/routes';\n\ninterface KapWindowOptions<State> extends Electron.BrowserWindowConstructorOptions {\n  route: string;\n  waitForMount?: boolean;\n  initialState?: State;\n  menu?: MenuModifier;\n  dock?: boolean;\n}\n\n// TODO: remove this when all windows use KapWindow\napp.on('browser-window-focus', (_, window) => {\n  if (!KapWindow.fromId(window.id)) {\n    Menu.setApplicationMenu(Menu.buildFromTemplate(defaultApplicationMenu()));\n  }\n});\n\n// Has to be named BrowserWindow because of\n// https://github.com/electron/electron/blob/master/lib/browser/api/browser-window.ts#L82\nexport default class KapWindow<State = any> {\n  static defaultOptions: Partial<KapWindowOptions<any>> = {\n    waitForMount: true,\n    dock: true,\n    menu: defaultMenu => defaultMenu\n  };\n\n  private static readonly windows = new Map<number, KapWindow>();\n\n  browserWindow: BrowserWindow;\n  state?: State;\n  menu: Menu = Menu.buildFromTemplate(defaultApplicationMenu());\n  readonly id: number;\n\n  private readonly readyPromise: Promise<void>;\n  private readonly cleanupMethods: Array<() => void> = [];\n  private readonly options: KapWindowOptions<State>;\n\n  constructor(private readonly props: KapWindowOptions<State>) {\n    const {\n      route,\n      waitForMount,\n      initialState,\n      ...rest\n    } = props;\n\n    this.browserWindow = new BrowserWindow({\n      ...rest,\n      webPreferences: {\n        nodeIntegration: true,\n        enableRemoteModule: true,\n        contextIsolation: false,\n        ...rest.webPreferences\n      },\n      show: false\n    });\n\n    this.id = this.browserWindow.id;\n    KapWindow.windows.set(this.id, this);\n\n    this.cleanupMethods = [];\n    this.options = {\n      ...KapWindow.defaultOptions,\n      ...props\n    };\n\n    this.state = initialState;\n    this.generateMenu();\n    this.readyPromise = this.setupWindow();\n  }\n\n  static getAllWindows() {\n    return [...this.windows.values()];\n  }\n\n  static fromId(id: number) {\n    return this.windows.get(id);\n  }\n\n  get webContents() {\n    return this.browserWindow.webContents;\n  }\n\n  cleanup = () => {\n    KapWindow.windows.delete(this.id);\n\n    for (const method of this.cleanupMethods) {\n      method();\n    }\n  };\n\n  callRenderer = async <T, R>(channel: string, data?: T) => {\n    return ipc.callRenderer<T, R>(this.browserWindow, channel, data);\n  };\n\n  answerRenderer = <T, R>(channel: string, callback: (data: T, window: electron.BrowserWindow) => R) => {\n    this.cleanupMethods.push(ipc.answerRenderer(this.browserWindow, channel, callback));\n  };\n\n  setState = (partialState: State) => {\n    this.state = {\n      ...this.state,\n      ...partialState\n    };\n\n    this.callRenderer('kap-window-state', this.state);\n  };\n\n  whenReady = async () => {\n    return this.readyPromise;\n  };\n\n  private readonly generateMenu = () => {\n    this.menu = Menu.buildFromTemplate(\n      customApplicationMenu(this.options.menu!)\n    );\n  };\n\n  private async setupWindow() {\n    const {waitForMount} = this.options;\n\n    KapWindow.windows.set(this.id, this);\n\n    this.browserWindow.on('show', () => {\n      if (this.options.dock && !app.dock.isVisible) {\n        app.dock.show();\n      }\n    });\n\n    this.browserWindow.on('close', this.cleanup);\n    this.browserWindow.on('closed', this.cleanup);\n\n    this.browserWindow.on('focus', () => {\n      this.generateMenu();\n      Menu.setApplicationMenu(this.menu);\n    });\n\n    this.webContents.on('did-finish-load', async () => {\n      if (this.state) {\n        this.callRenderer('kap-window-state', this.state);\n      }\n    });\n\n    this.answerRenderer('kap-window-state', () => this.state);\n\n    loadRoute(this.browserWindow, this.props.route);\n\n    if (waitForMount) {\n      return new Promise<void>(resolve => {\n        this.answerRenderer('kap-window-mount', () => {\n          if (!this.browserWindow.isVisible()) {\n            this.browserWindow.show();\n          }\n\n          resolve();\n        });\n      });\n    }\n\n    await pEvent(this.webContents, 'did-finish-load');\n    this.browserWindow.show();\n  }\n\n  // Use this around any call that causes:\n  // TypeError: Object has been destroyed\n  // private readonly executeIfNotDestroyed = (callback: () => void) => {\n  //   if (!this.browserWindow.isDestroyed()) {\n  //     callback();\n  //   }\n  // };\n}\n"
  },
  {
    "path": "main/windows/load.ts",
    "content": "import './editor';\nimport './cropper';\nimport './config';\nimport './dialog';\nimport './exports';\nimport './preferences';\n"
  },
  {
    "path": "main/windows/manager.ts",
    "content": "import type {BrowserWindow} from 'electron';\nimport {MacWindow} from '../utils/windows';\nimport type {Video} from '../video';\nimport type {DialogOptions} from './dialog';\nimport type {PreferencesWindowOptions} from './preferences';\n\nexport interface EditorManager {\n  open: (video: Video) => Promise<void>;\n  areAnyBlocking: () => boolean;\n}\n\nexport interface CropperManager {\n  open: () => Promise<void>;\n  close: () => void;\n  disable: () => void;\n  setRecording: () => void;\n  isOpen: () => boolean;\n  selectApp: (window: MacWindow, activateWindow: (ownerName: string) => Promise<void>) => void;\n}\n\nexport interface ConfigManager {\n  open: (pluginName: string) => Promise<void>;\n}\n\nexport interface DialogManager {\n  open: (options: DialogOptions) => Promise<number | void>;\n}\n\nexport interface ExportsManager {\n  open: () => Promise<BrowserWindow>;\n  get: () => BrowserWindow | undefined;\n}\n\nexport interface PreferencesManager {\n  open: (options?: PreferencesWindowOptions) => Promise<BrowserWindow>;\n  close: () => void;\n}\n\nexport class WindowManager {\n  editor?: EditorManager;\n  cropper?: CropperManager;\n  config?: ConfigManager;\n  dialog?: DialogManager;\n  exports?: ExportsManager;\n  preferences?: PreferencesManager;\n\n  setEditor = (editorManager: EditorManager) => {\n    this.editor = editorManager;\n  };\n\n  setCropper = (cropperManager: CropperManager) => {\n    this.cropper = cropperManager;\n  };\n\n  setConfig = (configManager: ConfigManager) => {\n    this.config = configManager;\n  };\n\n  setDialog = (dialogManager: DialogManager) => {\n    this.dialog = dialogManager;\n  };\n\n  setExports = (exportsManager: ExportsManager) => {\n    this.exports = exportsManager;\n  };\n\n  setPreferences = (preferencesManager: PreferencesManager) => {\n    this.preferences = preferencesManager;\n  };\n}\n\nexport const windowManager = new WindowManager();\n"
  },
  {
    "path": "main/windows/preferences.ts",
    "content": "import {BrowserWindow} from 'electron';\nimport {promisify} from 'util';\nimport pEvent from 'p-event';\n\nimport {ipcMain as ipc} from 'electron-better-ipc';\nimport {loadRoute} from '../utils/routes';\nimport {track} from '../common/analytics';\nimport {windowManager} from './manager';\n\nlet prefsWindow: BrowserWindow | undefined;\n\nexport type PreferencesWindowOptions = any;\n\nconst openPrefsWindow = async (options?: PreferencesWindowOptions) => {\n  track('preferences/opened');\n  windowManager.cropper?.close();\n\n  if (prefsWindow) {\n    if (options) {\n      ipc.callRenderer(prefsWindow, 'options', options);\n    }\n\n    prefsWindow.show();\n    return prefsWindow;\n  }\n\n  prefsWindow = new BrowserWindow({\n    title: 'Preferences',\n    width: 480,\n    height: 480,\n    resizable: false,\n    minimizable: false,\n    maximizable: false,\n    fullscreenable: false,\n    titleBarStyle: 'hiddenInset',\n    show: false,\n    frame: false,\n    transparent: true,\n    vibrancy: 'window',\n    webPreferences: {\n      nodeIntegration: true,\n      enableRemoteModule: true,\n      contextIsolation: false\n    }\n  });\n\n  const titlebarHeight = 85;\n  prefsWindow.setSheetOffset(titlebarHeight);\n\n  prefsWindow.on('close', () => {\n    prefsWindow = undefined;\n  });\n\n  loadRoute(prefsWindow, 'preferences');\n\n  await pEvent(prefsWindow.webContents, 'did-finish-load');\n\n  if (options) {\n    ipc.callRenderer(prefsWindow, 'options', options);\n  }\n\n  ipc.callRenderer(prefsWindow, 'mount');\n\n  // @ts-expect-error\n  await promisify(ipc.answerRenderer)('preferences-ready');\n\n  prefsWindow.show();\n  return prefsWindow;\n};\n\nconst closePrefsWindow = () => {\n  if (prefsWindow) {\n    prefsWindow.close();\n  }\n};\n\nipc.answerRenderer('open-preferences', openPrefsWindow);\n\nwindowManager.setPreferences({\n  open: openPrefsWindow,\n  close: closePrefsWindow\n});\n"
  },
  {
    "path": "maintaining.md",
    "content": "# Maintaining\n\n## Developing Kap\n\nRun `yarn dev` in one terminal tab to start watch mode, and in another tab, run `yarn start` to launch Kap.\n\nWe strongly recommend installing an [XO editor plugin](https://github.com/sindresorhus/xo#editor-plugins) for JavaScript linting and a [Stylelint editor plugin](https://github.com/stylelint/stylelint/blob/master/docs/user-guide/integrations/editor.md) for CSS linting. Both of these support auto-fix on save.\n\n## Releasing a new version\n\n*(You can do all the steps on github.com)*\n\n- Go to https://github.com/wulkano/kap/releases\n- Click `Draft a new release`\n- Write the new version, prefixed with `v`, in the `Tag version` field (Example: `v2.0.0`)\n- Leave the `Release title` field blank\n- Write release notes\n- Click `Save draft`\n- Change `version` [here](https://github.com/wulkano/kap/blob/main/package.json#L4) to the new version and use the version number as the commit title (Example: `2.0.0`)\n- CircleCI will now build the app and add the binaries to the release\n- When CircleCI has attached the binaries to the release, click `Edit` on the release, and then click `Publish release`\n\n## Releasing a new beta version\n\n- Check out the `beta` branch: `git checkout beta`\n- Rebase from the `main` branch: `git pull --rebase origin main`\n- Change the `version` number in `package.json`\n- Amend the \"Beta build customizations\" commit: `git add . && git commit --amend`\n- Force push to the `beta` branch: `git push --force`\n- Tag a release with the version number in package.json and push it: `git tag -a \"v2.0.0-beta.3\" -m \"v2.0.0-beta.3\" && git push --follow-tags`\n- Wait for CircleCI to add the binaries to a new GitHub Releases draft\n- Go to the release draft that is created for you, check `This is a pre-release`, and press `Publish release`\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"kap\",\n  \"productName\": \"Kap\",\n  \"version\": \"3.6.0\",\n  \"description\": \"An open-source screen recorder built with web technology\",\n  \"license\": \"MIT\",\n  \"repository\": \"wulkano/kap\",\n  \"homepage\": \"https://getkap.co\",\n  \"author\": {\n    \"name\": \"Wulkano\",\n    \"email\": \"hello@wulkano.com\",\n    \"url\": \"https://wulkano.com\"\n  },\n  \"private\": true,\n  \"main\": \"dist-js/index.js\",\n  \"scripts\": {\n    \"lint\": \"xo\",\n    \"lint:fix\": \"xo --fix\",\n    \"test:main\": \"TS_NODE_PROJECT=test/tsconfig.json ava\",\n    \"test:ci\": \"yarn test:main --tap | tap-xunit > ~/reports/ava.xml\",\n    \"test\": \"yarn lint && yarn test:main\",\n    \"start\": \"tsc && run-electron .\",\n    \"build-main\": \"tsc\",\n    \"build-renderer\": \"next build renderer && next export renderer\",\n    \"build\": \"yarn build-main && yarn build-renderer\",\n    \"dist\": \"npm run build && electron-builder\",\n    \"pack\": \"npm run build && electron-builder --dir\",\n    \"postinstall\": \"electron-builder install-app-deps\",\n    \"sentry-version\": \"echo \\\"$npm_package_name@$npm_package_version\\\"\",\n    \"dev\": \"next dev renderer\"\n  },\n  \"bundle\": {\n    \"name\": \"Kap\"\n  },\n  \"dependencies\": {\n    \"@sentry/browser\": \"^6.2.2\",\n    \"@sentry/electron\": \"^2.4.0\",\n    \"@sindresorhus/to-milliseconds\": \"^1.2.0\",\n    \"ajv\": \"^6.12.2\",\n    \"aperture\": \"^6.1.2\",\n    \"base64-img\": \"^1.0.4\",\n    \"classnames\": \"^2.2.6\",\n    \"clean-stack\": \"^3.0.1\",\n    \"cp-file\": \"^9.0.0\",\n    \"delay\": \"^5.0.0\",\n    \"electron-better-ipc\": \"^1.1.1\",\n    \"electron-log\": \"^4.3.2\",\n    \"electron-next\": \"^3.1.5\",\n    \"electron-notarize\": \"^1.1.1\",\n    \"electron-store\": \"^7.0.2\",\n    \"electron-timber\": \"^0.5.1\",\n    \"electron-updater\": \"^4.3.8\",\n    \"electron-util\": \"^0.14.2\",\n    \"ensure-error\": \"^3.0.1\",\n    \"execa\": \"5.0.0\",\n    \"ffmpeg-static\": \"^4.4.1\",\n    \"gifsicle\": \"^5.2.0\",\n    \"got\": \"^9.6.0\",\n    \"insight\": \"^0.10.3\",\n    \"is-online\": \"^8.4.0\",\n    \"lodash\": \"^4.17.21\",\n    \"mac-open-with\": \"^1.2.3\",\n    \"mac-screen-capture-permissions\": \"^1.1.0\",\n    \"mac-windows\": \"^1.0.0\",\n    \"macos-audio-devices\": \"^1.4.0\",\n    \"macos-version\": \"^5.2.1\",\n    \"make-dir\": \"^3.1.0\",\n    \"moment\": \"^2.29.1\",\n    \"move-file\": \"^2.0.0\",\n    \"nearest-normal-aspect-ratio\": \"^1.2.1\",\n    \"node-mac-app-icon\": \"^1.4.0\",\n    \"object-hash\": \"^2.1.1\",\n    \"p-cancelable\": \"^2.1.0\",\n    \"p-event\": \"^4.2.0\",\n    \"package-json\": \"^6.5.0\",\n    \"pify\": \"^5.0.0\",\n    \"plist\": \"^3.0.4\",\n    \"pretty-bytes\": \"^5.6.0\",\n    \"pretty-ms\": \"^7.0.1\",\n    \"prop-types\": \"^15.7.2\",\n    \"react\": \"^17.0.1\",\n    \"react-dom\": \"^17.0.1\",\n    \"react-linkify\": \"^0.2.2\",\n    \"react-tooltip\": \"^4.2.19\",\n    \"read-pkg\": \"^5.2.0\",\n    \"semver\": \"^7.3.4\",\n    \"string-math\": \"^1.2.2\",\n    \"tempy\": \"^1.0.0\",\n    \"tildify\": \"^2.0.0\",\n    \"tmp\": \"^0.2.0\",\n    \"unstated\": \"^1.2.0\",\n    \"unstated-next\": \"^1.1.0\",\n    \"yarn\": \"^1.22.10\"\n  },\n  \"devDependencies\": {\n    \"@babel/core\": \"^7.12.16\",\n    \"@babel/eslint-parser\": \"^7.12.16\",\n    \"@sindresorhus/tsconfig\": \"^0.7.0\",\n    \"@types/ffmpeg-static\": \"^3.0.0\",\n    \"@types/got\": \"9.6.12\",\n    \"@types/insight\": \"^0.8.0\",\n    \"@types/lodash\": \"^4.14.168\",\n    \"@types/module-alias\": \"^2.0.0\",\n    \"@types/node\": \"^14.11.10\",\n    \"@types/object-hash\": \"^1.3.4\",\n    \"@types/react\": \"^17.0.3\",\n    \"@types/sinon\": \"^9.0.10\",\n    \"@typescript-eslint/eslint-plugin\": \"^4.15.0\",\n    \"@typescript-eslint/parser\": \"^4.15.0\",\n    \"ava\": \"^3.15.0\",\n    \"babel-eslint\": \"^10.1.0\",\n    \"electron\": \"13.6.9\",\n    \"electron-builder\": \"^23.3.3\",\n    \"electron-builder-notarize\": \"^1.4.0\",\n    \"eslint-config-xo\": \"^0.35.0\",\n    \"eslint-config-xo-react\": \"^0.24.0\",\n    \"eslint-config-xo-typescript\": \"^0.38.0\",\n    \"eslint-plugin-react\": \"^7.22.0\",\n    \"eslint-plugin-react-hooks\": \"^4.2.0\",\n    \"husky\": \"^4.2.5\",\n    \"module-alias\": \"^2.2.2\",\n    \"next\": \"^10.0.8\",\n    \"run-electron\": \"^1.0.0\",\n    \"sinon\": \"^9.2.4\",\n    \"tap-xunit\": \"^2.4.1\",\n    \"ts-node\": \"^10.4.0\",\n    \"type-fest\": \"^2.11.1\",\n    \"typed-emitter\": \"^1.3.1\",\n    \"typescript\": \"^4.0.3\",\n    \"unique-string\": \"^2.0.0\",\n    \"xo\": \"^0.38.2\"\n  },\n  \"_moduleAliases\": {\n    \"electron\": \"test/mocks/electron.ts\"\n  },\n  \"ava\": {\n    \"files\": [\n      \"test/**/*.ts\",\n      \"test/**/*.js\",\n      \"!test/helpers\",\n      \"!test/mocks\"\n    ],\n    \"extensions\": [\n      \"ts\"\n    ],\n    \"verbose\": true,\n    \"timeout\": \"5m\",\n    \"failFast\": true,\n    \"require\": [\n      \"ts-node/register\",\n      \"module-alias/register\"\n    ]\n  },\n  \"xo\": {\n    \"extends\": \"xo-react\",\n    \"space\": 2,\n    \"envs\": [\n      \"node\",\n      \"browser\"\n    ],\n    \"rules\": {\n      \"template-curly-spacing\": \"off\",\n      \"import/no-extraneous-dependencies\": \"off\",\n      \"import/no-unassigned-import\": \"off\",\n      \"import/no-named-as-default-member\": \"off\",\n      \"react/jsx-closing-tag-location\": \"off\",\n      \"react/require-default-props\": \"off\",\n      \"react/jsx-curly-brace-presence\": \"off\",\n      \"react/static-property-placement\": \"off\",\n      \"react/react-in-jsx-scope\": \"off\",\n      \"react/boolean-prop-naming\": \"off\",\n      \"unicorn/prefer-set-has\": \"off\",\n      \"ava/use-test\": \"off\",\n      \"import/extensions\": \"off\",\n      \"node/file-extension-in-import\": \"off\"\n    },\n    \"ignores\": [\n      \"dist-js\",\n      \"dist\",\n      \"renderer/.next\",\n      \"renderer/out\",\n      \"renderer/next.config.js\"\n    ],\n    \"overrides\": [\n      {\n        \"files\": [\n          \"**/*.js\",\n          \"**/*.jsx\"\n        ],\n        \"parser\": \"babel-eslint\"\n      },\n      {\n        \"files\": [\n          \"**/*.ts\",\n          \"**/*.tsx\"\n        ],\n        \"extends\": [\n          \"xo-react\",\n          \"xo-typescript\"\n        ],\n        \"parserOptions\": {\n          \"project\": [\n            \"tsconfig.json\",\n            \"renderer/tsconfig.json\",\n            \"test/tsconfig.json\"\n          ]\n        },\n        \"rules\": {\n          \"react-hooks/exhaustive-deps\": \"off\",\n          \"@typescript-eslint/no-dynamic-delete\": \"off\",\n          \"@typescript-eslint/no-var-requires\": \"off\",\n          \"@typescript-eslint/no-floating-promises\": \"off\",\n          \"@typescript-eslint/no-implicit-any-catch\": \"off\",\n          \"@typescript-eslint/restrict-template-expressions\": \"off\",\n          \"no-await-in-loop\": \"off\",\n          \"react/prop-types\": \"off\",\n          \"@typescript-eslint/no-confusing-void-expression\": \"off\",\n          \"@typescript-eslint/indent\": [\n            \"error\",\n            2\n          ]\n        }\n      },\n      {\n        \"files\": [\n          \"test/**/*.ts\"\n        ],\n        \"rules\": {\n          \"@typescript-eslint/consistent-type-assertions\": \"off\",\n          \"@typescript-eslint/member-ordering\": \"off\",\n          \"import/no-anonymous-default-export\": \"off\",\n          \"@typescript-eslint/no-extraneous-class\": \"off\",\n          \"@typescript-eslint/no-empty-function\": \"off\"\n        }\n      }\n    ]\n  },\n  \"husky\": {\n    \"hooks\": {\n      \"pre-commit\": \"yarn lint\",\n      \"pre-push\": \"yarn lint\"\n    }\n  },\n  \"build\": {\n    \"appId\": \"com.wulkano.kap\",\n    \"afterSign\": \"electron-builder-notarize\",\n    \"protocols\": {\n      \"name\": \"kap\",\n      \"schemes\": [\n        \"kap\"\n      ]\n    },\n    \"files\": [\n      \"static\",\n      \"dist-js/**/*\",\n      \"!renderer\",\n      \"renderer/out\"\n    ],\n    \"mac\": {\n      \"electronUpdaterCompatibility\": \">=2.16\",\n      \"category\": \"public.app-category.productivity\",\n      \"minimumSystemVersion\": \"10.12.0\",\n      \"darkModeSupport\": true,\n      \"hardenedRuntime\": true,\n      \"entitlements\": \"./build/entitlements.mac.inherit.plist\",\n      \"extendInfo\": {\n        \"NSMicrophoneUsageDescription\": \"Kap needs access to the microphone to be able to record audio for screen recordings.\",\n        \"NSCameraUsageDescription\": \"A Kap plugin wants to use the camera.\",\n        \"NSUserNotificationAlertStyle\": \"alert\",\n        \"CFBundleDocumentTypes\": [\n          {\n            \"CFBundleTypeName\": \"Video\",\n            \"CFBundleTypeRole\": \"Viewer\",\n            \"LSHandlerRank\": \"Alternate\",\n            \"LSItemContentTypes\": [\n              \"public.mpeg-4\",\n              \"com.apple.quicktime-movie\"\n            ]\n          }\n        ]\n      },\n      \"target\": {\n        \"target\": \"default\",\n        \"arch\": [\n          \"x64\",\n          \"arm64\"\n        ]\n      }\n    },\n    \"dmg\": {\n      \"artifactName\": \"${productName}-${version}-${arch}.${ext}\",\n      \"iconSize\": 160,\n      \"contents\": [\n        {\n          \"x\": 180,\n          \"y\": 170\n        },\n        {\n          \"x\": 480,\n          \"y\": 170,\n          \"type\": \"link\",\n          \"path\": \"/Applications\"\n        }\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "renderer/components/action-bar/controls/advanced.js",
    "content": "import PropTypes from 'prop-types';\nimport React from 'react';\nimport css from 'styled-jsx/css';\n\nimport {\n  SwapIcon,\n  BackIcon,\n  LinkIcon,\n  DropdownArrowIcon\n} from '../../../vectors';\n\nimport {connect, ActionBarContainer, CropperContainer} from '../../../containers';\n\nimport {\n  handleWidthInput,\n  handleHeightInput,\n  buildAspectRatioMenu,\n  minHeight,\n  minWidth,\n  handleKeyboardActivation,\n  RATIOS\n} from '../../../utils/inputs';\n\nimport KeyboardNumberInput from '../../keyboard-number-input';\n\nconst advancedStyles = css`\n  .advanced {\n    height: 64px;\n    display: flex;\n    flex: 1;\n    align-items: center;\n    padding: 0 8px;\n  }\n`;\n\nconst {className: keyboardInputClass, styles: keyboardInputStyles} = css.resolve`\n  height: 32px;\n  border: 1px solid var(--input-border-color);\n  background: var(--input-background-color);\n  color: var(--title-color);\n  text-align: left;\n  font-size: 12px;\n  transition: border 0.12s ease-in-out;\n  box-sizing: border-box;\n  padding: 8px;\n  border-radius: 4px;\n  margin-right: 8px;\n  width: 64px;\n\n  :focus {\n    outline: none;\n    border: 1px solid var(--input-focus-border-color);\n  }\n\n  :hover {\n    border-color: var(--input-hover-border-color);\n  }\n`;\n\nconst AdvancedControls = {};\n\nconst stopPropagation = event => event.stopPropagation();\n\nclass Left extends React.Component {\n  state = {};\n\n  select = React.createRef();\n\n  static getDerivedStateFromProps(nextProps, previousState) {\n    const {ratio, isResizing, setRatio} = nextProps;\n\n    if (ratio !== previousState.ratio && !isResizing) {\n      return {\n        ratio,\n        menu: buildAspectRatioMenu({setRatio, ratio})\n      };\n    }\n\n    return null;\n  }\n\n  openMenu = () => {\n    const {ratio} = this.props;\n    const boundingRect = this.select.current.getBoundingClientRect();\n    const {top, left} = boundingRect;\n    const selectedRatio = ratio.join(':');\n    const index = RATIOS.indexOf(selectedRatio);\n    const positioningItem = index > -1 ? index : RATIOS.length;\n\n    this.state.menu.popup({\n      x: Math.round(left),\n      y: Math.round(top) + 6,\n      positioningItem\n    });\n  };\n\n  render() {\n    const {advanced, toggleAdvanced, toggleRatioLock, ratioLocked, ratio = []} = this.props;\n\n    return (\n      <div className=\"advanced\">\n        <div className=\"back\">\n          <BackIcon tabIndex={advanced ? 0 : -1} onClick={toggleAdvanced}/>\n        </div>\n        <div\n          ref={this.select}\n          className=\"select\"\n          tabIndex={advanced ? 0 : -1}\n          onClick={this.openMenu}\n          onMouseDown={stopPropagation}\n          onKeyDown={handleKeyboardActivation(this.openMenu, {isMenu: true})}\n        >\n          <span>{ratio[0]}:{ratio[1]}</span>\n          <DropdownArrowIcon size=\"18px\"/>\n        </div>\n        <div className=\"link\" tabIndex={advanced ? 0 : -1} onKeyPress={handleKeyboardActivation(toggleRatioLock)}>\n          <LinkIcon active={ratioLocked} onClick={() => toggleRatioLock()}/>\n        </div>\n        <style jsx>{advancedStyles}</style>\n        <style jsx>{`\n          .back {\n            padding: 0 8px;\n          }\n\n          .select {\n            border: 1px solid var(--input-border-color);\n            background: var(--input-background-color);\n            color: var(--title-color);\n            border-radius: 4px;\n            font-size: 0.7rem;\n            width: 96px;\n            margin: 0 8px;\n            transition: border 0.12s ease-in-out;\n            display: flex;\n            align-items: center;\n            padding: 8px;\n            height: 32px;\n            box-sizing: border-box;\n          }\n\n          .select:focus {\n            outline: none;\n            border: 1px solid var(--input-focus-border-color);\n          }\n\n          .select span {\n            width: 64px;\n            line-height: 16px;\n            font-size: 12px;\n          }\n\n          .select:hover {\n            border-color: var(--input-hover-border-color);\n          }\n\n          .link {\n            width: 32px;\n            height: 32px;\n            padding: 3px 3px;\n            box-sizing: border-box;\n            background: ${ratioLocked ? 'var(--button-active-color)' : 'var(--cropper-button-background-color)'};\n            border: 1px solid var(--input-border-color);\n            border-radius: 4px;\n          }\n\n          .link:focus {\n            outline: none;\n            border: 1px solid var(--input-focus-border-color);\n          }\n        `}</style>\n      </div>\n    );\n  }\n}\n\nLeft.propTypes = {\n  toggleAdvanced: PropTypes.elementType.isRequired,\n  toggleRatioLock: PropTypes.elementType.isRequired,\n  ratioLocked: PropTypes.bool,\n  isResizing: PropTypes.bool,\n  ratio: PropTypes.array,\n  setRatio: PropTypes.elementType.isRequired,\n  advanced: PropTypes.bool\n};\n\nAdvancedControls.Left = connect(\n  [ActionBarContainer, CropperContainer],\n  ({ratioLocked, advanced}, {ratio, isResizing}) => ({advanced, ratio, ratioLocked, isResizing}),\n  ({toggleAdvanced, toggleRatioLock}, {setRatio}) => ({toggleAdvanced, toggleRatioLock, setRatio})\n)(Left);\n\nclass Right extends React.Component {\n  constructor(props) {\n    super(props);\n\n    this.widthInput = React.createRef();\n    this.heightInput = React.createRef();\n  }\n\n  onWidthChange = (event, {ignoreEmpty} = {}) => {\n    const {bounds, height, setBounds, ratioLocked, ratio, setWidth} = this.props;\n    const {value} = event.currentTarget;\n    const {heightInput, widthInput} = this;\n\n    setWidth(value);\n    handleWidthInput({\n      bounds,\n      height,\n      setBounds,\n      ratioLocked,\n      ratio,\n      value,\n      widthInput: widthInput.current.getRef(),\n      heightInput: heightInput.current.getRef(),\n      ignoreEmpty\n    });\n  };\n\n  onHeightChange = (event, {ignoreEmpty} = {}) => {\n    const {bounds, width, setBounds, ratioLocked, ratio, setHeight} = this.props;\n    const {value} = event.currentTarget;\n    const {heightInput, widthInput} = this;\n\n    setHeight(value);\n    handleHeightInput({\n      bounds,\n      width,\n      setBounds,\n      ratioLocked,\n      ratio,\n      value,\n      widthInput: widthInput.current.getRef(),\n      heightInput: heightInput.current.getRef(),\n      ignoreEmpty\n    });\n  };\n\n  onWidthBlur = event => {\n    this.onWidthChange(event, {ignoreEmpty: false});\n    handleWidthInput.flush();\n  };\n\n  onHeightBlur = event => {\n    this.onHeightChange(event, {ignoreEmpty: false});\n    handleHeightInput.flush();\n  };\n\n  render() {\n    const {swapDimensions, width, height, screenWidth, screenHeight, advanced} = this.props;\n\n    return (\n      <div className=\"advanced\">\n        <KeyboardNumberInput\n          ref={this.widthInput}\n          className={keyboardInputClass}\n          name=\"width\"\n          size=\"5\"\n          min={minWidth}\n          max={screenWidth}\n          maxLength=\"5\"\n          value={width}\n          tabIndex={advanced ? 0 : -1}\n          onChange={this.onWidthChange}\n          onBlur={this.onWidthBlur}\n          onMouseDown={stopPropagation}\n        />\n        <div className=\"swap\" tabIndex={advanced ? 0 : -1} onKeyPress={handleKeyboardActivation(swapDimensions)}>\n          <SwapIcon onClick={swapDimensions}/>\n        </div>\n        <KeyboardNumberInput\n          ref={this.heightInput}\n          className={keyboardInputClass}\n          name=\"height\"\n          size=\"5\"\n          min={minHeight}\n          max={screenHeight}\n          maxLength=\"5\"\n          value={height}\n          tabIndex={advanced ? 0 : -1}\n          onChange={this.onHeightChange}\n          onBlur={this.onHeightBlur}\n          onMouseDown={stopPropagation}\n        />\n        {keyboardInputStyles}\n        <style jsx>{advancedStyles}</style>\n        <style jsx>{`\n          .swap {\n            width: 32px;\n            height: 32px;\n            padding: 3px 3px;\n            background: var(--cropper-button-background-color);\n            box-sizing: border-box;\n            border: 1px solid var(--input-border-color);\n            border-radius: 4px;\n            margin-right: 8px;\n          }\n\n          .swap:focus {\n            outline: none;\n            border: 1px solid var(--input-focus-border-color);\n          }\n        `}</style>\n      </div>\n    );\n  }\n}\n\nRight.propTypes = {\n  bounds: PropTypes.object,\n  width: PropTypes.string,\n  height: PropTypes.string,\n  ratio: PropTypes.array,\n  ratioLocked: PropTypes.bool,\n  advanced: PropTypes.bool,\n  setBounds: PropTypes.elementType.isRequired,\n  swapDimensions: PropTypes.elementType.isRequired,\n  setWidth: PropTypes.elementType.isRequired,\n  setHeight: PropTypes.elementType.isRequired,\n  screenWidth: PropTypes.number,\n  screenHeight: PropTypes.number\n};\n\nAdvancedControls.Right = connect(\n  [CropperContainer, ActionBarContainer],\n  (\n    {x, y, ratio, width, height, screenWidth, screenHeight},\n    {cropperWidth, cropperHeight, ratioLocked, advanced}\n  ) => ({\n    screenHeight,\n    screenWidth,\n    bounds: {x, y, width, height},\n    width: cropperWidth,\n    height: cropperHeight,\n    ratio,\n    ratioLocked,\n    advanced\n  }),\n  (\n    {setBounds, swapDimensions},\n    {setWidth, setHeight}\n  ) => ({\n    setBounds,\n    swapDimensions,\n    setWidth,\n    setHeight\n  })\n)(Right);\n\nexport default AdvancedControls;\n"
  },
  {
    "path": "renderer/components/action-bar/controls/main.js",
    "content": "import electron from 'electron';\nimport PropTypes from 'prop-types';\nimport React from 'react';\nimport css from 'styled-jsx/css';\n\nimport IconMenu from '../../icon-menu';\nimport {\n  MoreIcon,\n  CropIcon,\n  ApplicationsIcon,\n  FullscreenIcon,\n  ExitFullscreenIcon\n} from '../../../vectors';\nimport {connect, ActionBarContainer, CropperContainer} from '../../../containers';\n\nconst mainStyle = css`\n  .main {\n    height: 64px;\n    display: flex;\n    flex: 1;\n    align-items: center;\n  }\n`;\n\nconst MainControls = {};\n\nconst remote = electron.remote || false;\nlet menu;\n\nconst buildMenu = async ({selectedApp}) => {\n  const {buildWindowsMenu} = remote.require('./utils/windows');\n  menu = await buildWindowsMenu(selectedApp);\n};\n\nclass Left extends React.Component {\n  state = {};\n\n  static getDerivedStateFromProps(nextProps, previousState) {\n    const {selectedApp} = nextProps;\n\n    if (selectedApp !== previousState.selectedApp) {\n      buildMenu({selectedApp});\n      return {selectedApp};\n    }\n\n    return null;\n  }\n\n  render() {\n    const {toggleAdvanced, selectedApp, advanced} = this.props;\n\n    return (\n      <div className=\"main\">\n        <div className=\"crop\">\n          <CropIcon tabIndex={advanced ? -1 : 0} onClick={toggleAdvanced}/>\n        </div>\n        <IconMenu isMenu icon={ApplicationsIcon} tabIndex={advanced ? -1 : 0} active={Boolean(selectedApp)} onOpen={menu && menu.popup}/>\n        <style jsx>{mainStyle}</style>\n        <style jsx>{`\n          .crop {\n            margin-left: 32px;\n            margin-right: 64px;\n            width: 24px;\n            height: 24px;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n          }\n        `}</style>\n      </div>\n    );\n  }\n}\n\nLeft.propTypes = {\n  toggleAdvanced: PropTypes.elementType.isRequired,\n  selectedApp: PropTypes.string,\n  advanced: PropTypes.bool\n};\n\nMainControls.Left = connect(\n  [CropperContainer, ActionBarContainer],\n  ({selectedApp}, {advanced}) => ({selectedApp, advanced}),\n  (_, {toggleAdvanced}) => ({toggleAdvanced})\n)(Left);\n\nclass Right extends React.Component {\n  onCogMenuClick = async () => {\n    const cogMenu = await electron.remote.require('./menus/cog').getCogMenu();\n    cogMenu.popup();\n  };\n\n  render() {\n    const {enterFullscreen, exitFullscreen, isFullscreen, advanced} = this.props;\n\n    return (\n      <div className=\"main\">\n        <div className=\"fullscreen\">\n          {\n            isFullscreen ?\n              <ExitFullscreenIcon active tabIndex={advanced ? -1 : 0} onClick={exitFullscreen}/> :\n              <FullscreenIcon tabIndex={advanced ? -1 : 0} onClick={enterFullscreen}/>\n          }\n        </div>\n        <IconMenu isMenu icon={MoreIcon} tabIndex={advanced ? -1 : 0} onOpen={this.onCogMenuClick}/>\n        <style jsx>{mainStyle}</style>\n        <style jsx>{`\n          .fullscreen {\n            margin-left: 56px;\n            margin-right: 64px;\n            height: 24px;\n          }\n        `}</style>\n      </div>\n    );\n  }\n}\n\nRight.propTypes = {\n  enterFullscreen: PropTypes.elementType.isRequired,\n  exitFullscreen: PropTypes.elementType.isRequired,\n  isFullscreen: PropTypes.bool,\n  advanced: PropTypes.bool\n};\n\nMainControls.Right = connect(\n  [CropperContainer, ActionBarContainer],\n  ({isFullscreen}, {advanced}) => ({isFullscreen, advanced}),\n  ({enterFullscreen, exitFullscreen}) => ({enterFullscreen, exitFullscreen})\n)(Right);\n\nexport default MainControls;\n"
  },
  {
    "path": "renderer/components/action-bar/index.js",
    "content": "import classNames from 'classnames';\nimport PropTypes from 'prop-types';\nimport React from 'react';\n\nimport {connect, CropperContainer, ActionBarContainer} from '../../containers';\n\nimport MainControls from './controls/main';\nimport AdvancedControls from './controls/advanced';\nimport RecordButton from './record-button';\n\nconst TRANSITION_DURATION = 0.2;\n\nclass ActionBar extends React.Component {\n  static defaultProps = {\n    cropperWidth: 0,\n    cropperHeight: 0,\n    width: 0,\n    height: 0,\n    x: 0,\n    y: 0\n  };\n\n  render() {\n    const {\n      startMoving,\n      x,\n      y,\n      width,\n      height,\n      hidden,\n      advanced,\n      isMoving,\n      cropperWidth,\n      cropperHeight\n    } = this.props;\n\n    const className = classNames('action-bar', {moving: isMoving, hidden, 'is-advanced': advanced});\n\n    return (\n      <div\n        className={className}\n        onMouseDown={startMoving}\n      >\n        <div className=\"actions\">\n          <div className=\"main\"><MainControls.Left/></div>\n          <div className=\"advanced\"><AdvancedControls.Left/></div>\n        </div>\n        <RecordButton\n          cropperExists={Boolean(cropperWidth && cropperHeight)}/>\n        <div className=\"actions\">\n          <div className=\"main\"><MainControls.Right/></div>\n          <div className=\"advanced\"><AdvancedControls.Right/></div>\n        </div>\n\n        <style jsx>{`\n            .action-bar {\n              position: fixed;\n              height: ${height}px;\n              width: ${width}px;\n              background: var(--action-bar-background);\n              border: var(--action-bar-border);\n              border-radius: 4px;\n              box-shadow: var(--action-bar-box-shadow);\n              z-index: 10;\n              top: ${y}px;\n              left: ${x}px;\n              display: flex;\n              align-items: center;\n              overflow: hidden;\n              opacity: 1;\n              transition: opacity 0.2s ease-out;\n              box-sizing: border-box;\n            }\n\n            .moving {\n              transition: none;\n            }\n\n            .hidden {\n              opacity: 0;\n            }\n\n            .actions {\n              position: relative;\n              flex: 1;\n              height: 64px;\n              width: 200px;\n            }\n\n            .main,\n            .advanced {\n              position: absolute;\n              top: 0;\n              left: 0;\n              width: 100%;\n              height: 100%;\n              transition: opacity ${TRANSITION_DURATION}s ease-in-out;\n            }\n\n            .main {\n              transition-delay: ${TRANSITION_DURATION}s;\n              z-index: 15;\n            }\n\n            .action-bar.is-advanced .main {\n              transition-delay: 0s;\n              opacity: 0;\n              z-index: 12;\n            }\n\n            .advanced {\n              transition-delay: 0s;\n              z-index: 12;\n              opacity: 0;\n            }\n\n            .action-bar.is-advanced .advanced {\n              transition-delay: ${TRANSITION_DURATION}s;\n              opacity: 1;\n              z-index: 15;\n            }\n        `}</style>\n      </div>\n    );\n  }\n}\n\nActionBar.propTypes = {\n  startMoving: PropTypes.elementType.isRequired,\n  x: PropTypes.number,\n  y: PropTypes.number,\n  width: PropTypes.number,\n  height: PropTypes.number,\n  hidden: PropTypes.bool,\n  advanced: PropTypes.bool,\n  isMoving: PropTypes.bool,\n  cropperWidth: PropTypes.number,\n  cropperHeight: PropTypes.number\n};\n\nexport default connect(\n  [ActionBarContainer, CropperContainer],\n  ({advanced, isMoving, width, height, x, y}, {willStartRecording, isPicking, isResizing, width: cropperWidth, isActive, height: cropperHeight, isMoving: cropperMoving}) => ({\n    advanced, width, height, x, y, isMoving, hidden: !isActive || cropperMoving || isResizing || isPicking || willStartRecording, cropperWidth, cropperHeight\n  }),\n  ({startMoving}) => ({startMoving})\n)(ActionBar);\n"
  },
  {
    "path": "renderer/components/action-bar/record-button.js",
    "content": "import electron from 'electron';\nimport React, {useState, useEffect} from 'react';\nimport PropTypes from 'prop-types';\nimport classNames from 'classnames';\n\nimport {connect, CropperContainer} from '../../containers';\nimport {handleKeyboardActivation} from '../../utils/inputs';\n\nconst getMediaNode = async deviceId => new Promise((resolve, reject) => {\n  navigator.getUserMedia({\n    audio: {deviceId}\n  }, stream => {\n    const audioContext = new AudioContext();\n    const analyser = audioContext.createAnalyser();\n    const microphone = audioContext.createMediaStreamSource(stream);\n    const javascriptNode = audioContext.createScriptProcessor(2048, 1, 1);\n\n    analyser.smoothingTimeConstant = 0.8;\n    analyser.fftSize = 1024;\n\n    microphone.connect(analyser);\n    analyser.connect(javascriptNode);\n    javascriptNode.connect(audioContext.destination);\n\n    resolve({javascriptNode, analyser});\n  }, reject);\n});\n\nconst RecordButton = ({\n  cropperExists,\n  x,\n  y,\n  width,\n  height,\n  screenWidth,\n  screenHeight,\n  displayId,\n  willStartRecording,\n  recordAudio,\n  audioInputDeviceId\n}) => {\n  const [showFirstRipple, setShowFirstRipple] = useState(false);\n  const [showSecondRipple, setShowSecondRipple] = useState(false);\n  const [shouldStop, setShouldStop] = useState(false);\n\n  useEffect(() => {\n    let node;\n\n    const connectToDevice = async () => {\n      try {\n        const {javascriptNode, analyser} = await getMediaNode(audioInputDeviceId);\n\n        javascriptNode.onaudioprocess = () => {\n          const array = new Uint8Array(analyser.frequencyBinCount);\n          analyser.getByteFrequencyData(array);\n          // eslint-disable-next-line unicorn/no-array-reduce\n          const avg = array.reduce((p, c) => p + c) / array.length;\n          if (avg >= 36) {\n            setShowFirstRipple(true);\n            setShowSecondRipple(true);\n            setShouldStop(false);\n          } else {\n            setShouldStop(true);\n          }\n        };\n\n        node = javascriptNode;\n      } catch (error) {\n        console.error('An error occurred when trying to get audio levels:', error);\n      }\n    };\n\n    if (recordAudio && audioInputDeviceId) {\n      connectToDevice();\n    }\n\n    return () => {\n      if (node && typeof node.disconnect === 'function') {\n        node.disconnect();\n      }\n    };\n  }, [recordAudio, audioInputDeviceId]);\n\n  const shouldFirstStop = () => {\n    if (shouldStop) {\n      setShowFirstRipple(false);\n    }\n  };\n\n  const shouldSecondStop = () => {\n    if (shouldStop) {\n      setShowSecondRipple(false);\n    }\n  };\n\n  const startRecording = event => {\n    event.stopPropagation();\n\n    if (cropperExists) {\n      const {remote} = electron;\n      const {startRecording} = remote.require('./aperture');\n\n      willStartRecording();\n\n      startRecording({\n        cropperBounds: {\n          x,\n          y,\n          width,\n          height\n        },\n        screenBounds: {\n          width: screenWidth,\n          height: screenHeight\n        },\n        displayId\n      });\n    }\n  };\n\n  return (\n    <div\n      className={classNames('container', {'cropper-exists': cropperExists})}\n      tabIndex={cropperExists ? 0 : -1}\n      onKeyDown={handleKeyboardActivation(startRecording)}\n    >\n      <div className=\"outer\" onMouseDown={startRecording}>\n        <div className=\"inner\">\n          {!cropperExists && <div className=\"fill\"/>}\n        </div>\n        {showFirstRipple && <div className=\"ripple first\" onAnimationIteration={shouldFirstStop}/>}\n        {showSecondRipple && <div className=\"ripple second\" onAnimationIteration={shouldSecondStop}/>}\n      </div>\n      <style jsx>{`\n            .container {\n              width: 64px;\n              height: 64px;\n              display: flex;\n              align-items: center;\n              justify-content: center;\n              outline: none;\n            }\n\n            .outer {\n              width: 48px;\n              height: 48px;\n              padding: 8px;\n              border-radius: 50%;\n              background: var(--record-button-background);\n              border: 2px solid var(--record-button-border-color);\n              display: flex;\n              align-items: center;\n              justify-content: center;\n              box-sizing: border-box;\n              position: relative;\n            }\n\n            .inner {\n              width: 24px;\n              height: 24px;\n              border-radius: 50%;\n              background: var(--record-button-inner-background${cropperExists ? '-cropper' : ''});\n              ${cropperExists ? '' : 'border: var(--record-button-inner-border-width) solid var(--record-button-inner-border);'}\n              box-sizing: border-box;\n            }\n\n            .fill {\n              width: 20px;\n              height: 20px;\n              border-radius: 50%;\n              background: var(--record-button-fill-background);\n              margin: 2px;\n            }\n\n            .ripple {\n              box-sizing: border-box;\n              border-radius: 50%;\n              border: 1px solid var(--record-button-ripple-color);\n              background: transparent;\n              position: absolute;\n              width: 100%;\n              height: 100%;\n            }\n\n            .first {\n              animation: ripple 1.8s linear infinite;\n            }\n\n            .second {\n              animation: ripple 1.8s linear 0.9s infinite;\n            }\n\n            .container.cropper-exists:focus .outer {\n              border: 2px solid var(--record-button-focus-outter-border);\n              background: var(--record-button-focus-outter-background);\n            }\n\n            .container.cropper-exists:focus .inner {\n              border-color: var(--record-button-border-color);\n              background: var(--record-button-focus-background${cropperExists ? '-cropper' : ''});\n            }\n\n            .container.cropper-exists:focus .fill {\n              background: var(--record-button-fill-background);\n            }\n\n            @keyframes ripple {\n              0% {\n                transform: scale(1);\n              }\n\n              100% {\n                transform: scale(1.3);\n                opacity: 0;\n              }\n            }\n        `}</style>\n    </div>\n  );\n};\n\nRecordButton.propTypes = {\n  cropperExists: PropTypes.bool,\n  x: PropTypes.number,\n  y: PropTypes.number,\n  width: PropTypes.number,\n  height: PropTypes.number,\n  screenWidth: PropTypes.number,\n  screenHeight: PropTypes.number,\n  displayId: PropTypes.number,\n  willStartRecording: PropTypes.elementType,\n  recordAudio: PropTypes.bool,\n  audioInputDeviceId: PropTypes.string\n};\n\nexport default connect(\n  [CropperContainer],\n  ({x, y, width, height, screenWidth, screenHeight, displayId, recordAudio, audioInputDeviceId}) => ({x, y, width, height, screenWidth, screenHeight, displayId, recordAudio, audioInputDeviceId}),\n  ({willStartRecording}) => ({willStartRecording})\n)(RecordButton);\n"
  },
  {
    "path": "renderer/components/config/index.js",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\n\nimport {connect, ConfigContainer} from '../../containers';\nimport Tab from './tab';\n\nclass Config extends React.Component {\n  render() {\n    const {\n      validators,\n      values,\n      onChange,\n      selectedTab,\n      selectTab,\n      closeWindow,\n      openConfig,\n      viewOnGithub,\n      serviceTitle\n    } = this.props;\n\n    if (!validators) {\n      return null;\n    }\n\n    return (\n      <div className=\"container\">\n        {\n          validators.length > 1 && (\n            <nav className=\"service-nav\">\n              {\n                validators.map((validator, index) => {\n                  return (\n                    <div\n                      key={validator.title}\n                      className={selectedTab === index ? 'selected' : ''}\n                      onClick={() => selectTab(index)}\n                    >\n                      {validator.title}\n                    </div>\n                  );\n                })\n              }\n            </nav>\n          )\n        }\n        <div className=\"tab-container\">\n          <div className=\"switcher\"/>\n          {\n            validators.map(validator => {\n              return (\n                <div key={validator.title} className=\"tab\">\n                  <Tab\n                    validator={validator}\n                    values={values}\n                    openConfig={openConfig}\n                    viewOnGithub={viewOnGithub}\n                    serviceTitle={serviceTitle}\n                    onChange={onChange}\n                  />\n                </div>\n              );\n            })\n          }\n        </div>\n        <footer>\n          <div className=\"fade\"/>\n          <button type=\"button\" onClick={closeWindow}>Done</button>\n        </footer>\n        <style jsx>{`\n          .container {\n            height: 100%;\n            width: 100%;\n            display: flex;\n            flex-direction: column;\n            word-break: break-word;\n          }\n\n          .service-nav {\n            height: 3.6rem;\n            padding: 0 16px;\n            display: flex;\n            align-items: center;\n            box-shadow: 0 1px 0 0 var(--row-divider-color), inset 0 1px 0 0 #fff;\n            z-index: 10;\n            max-width: 100%;\n            overflow-x: auto;\n          }\n\n          .service-nav div {\n            margin-right: 16px;\n            height: 100%;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n            padding-bottom: 2px;\n            font-size: 1.2rem;\n            color: var(--kap);\n            font-weight: 500;\n            width: 64px;\n          }\n\n          .service-nav div:last-child {\n            margin-right: 0;\n          }\n\n          .service-nav .selected {\n            border-bottom: 2px solid var(--kap);\n            padding-bottom: 0;\n          }\n\n          .tab-container {\n            flex: 1 1 272px;\n            display: flex;\n            overflow-x: hidden;\n          }\n\n          .tab {\n            overflow-y: auto;\n            width: 100%;\n            height: 100%;\n            flex-shrink: 0;\n          }\n\n          .switcher {\n            margin-left: ${-selectedTab * 100}%;\n            transition: margin 0.3s ease-in-out;\n          }\n\n          footer {\n            width: 100%;\n            display: flex;\n            position: relative;\n          }\n\n          footer .fade {\n            position: absolute;\n            background: linear-gradient(-180deg, rgba(255,255,255,0) 0%, var(--background-color) 100%);\n            width: 100%;\n            height: 16px;\n            top: 0;\n            transform: translateY(-100%);\n          }\n\n          footer button {\n            height: 32px;\n            line-height: 16px;\n            margin: 0 16px 16px 16px;\n            background: var(--button-color);\n            border-radius: 3px;\n            color: #fff;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n            flex: 1;\n            outline: none;\n            border: none;\n          }\n        `}</style>\n      </div>\n    );\n  }\n}\n\nConfig.propTypes = {\n  validators: PropTypes.arrayOf(PropTypes.elementType),\n  values: PropTypes.object,\n  onChange: PropTypes.elementType.isRequired,\n  selectedTab: PropTypes.number,\n  selectTab: PropTypes.elementType.isRequired,\n  closeWindow: PropTypes.elementType.isRequired,\n  openConfig: PropTypes.elementType.isRequired,\n  viewOnGithub: PropTypes.elementType.isRequired,\n  serviceTitle: PropTypes.string\n};\n\nexport default connect(\n  [ConfigContainer],\n  ({validators, values, selectedTab, serviceTitle}) => ({validators, values, selectedTab, serviceTitle}),\n  ({onChange, selectTab, closeWindow, openConfig, viewOnGithub}) => ({onChange, selectTab, closeWindow, openConfig, viewOnGithub})\n)(Config);\n"
  },
  {
    "path": "renderer/components/config/tab.js",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport Linkify from 'react-linkify';\n\nimport Item, {Link} from '../preferences/item';\nimport Select from '../preferences/item/select';\nimport Switch from '../preferences/item/switch';\nimport ColorPicker from '../preferences/item/color-picker';\nimport {OpenOnGithubIcon, OpenConfigIcon} from '../../vectors';\nimport ShortcutInput from '../preferences/shortcut-input';\n\nconst horizontalTypes = [\n  'boolean',\n  'hexColor'\n];\n\nconst ConfigInput = ({name, type, schema, value, onChange, hasErrors}) => {\n  if (type === 'keyboardShortcut') {\n    return (\n      <div>\n        <ShortcutInput tabIndex={0} shortcut={value} onChange={value => onChange(name, value)}/>\n        <style jsx>{`\n          div {\n            margin-top: 8px;\n          }\n        `}</style>\n      </div>\n    );\n  }\n\n  if (type === 'hexColor') {\n    return <ColorPicker value={value} name={name} hasErrors={hasErrors} onChange={value => onChange(name, value)}/>;\n  }\n\n  if (type === 'select') {\n    const options = schema.enum.map(value => ({label: value, value}));\n\n    if (!options.some(option => option.value === value)) {\n      const newValue = options[0] && options[0].value;\n      onChange(name, newValue);\n      return <Select full tabIndex={0} options={options} selected={newValue} onSelect={value => onChange(name, value)}/>;\n    }\n\n    return <Select full tabIndex={0} options={options} selected={value} onSelect={value => onChange(name, value)}/>;\n  }\n\n  if (type === 'boolean') {\n    return <Switch tabIndex={0} checked={value} onClick={() => onChange(name, !value)}/>;\n  }\n\n  const className = hasErrors ? 'has-errors' : '';\n  const handleChange = event => {\n    const value = type === 'number' ? Number.parseFloat(event.target.value) : event.currentTarget.value;\n    onChange(name, value);\n  };\n\n  return (\n    <div>\n      <input\n        className={className}\n        value={value || ''}\n        type={type === 'number' ? 'number' : 'text'}\n        onChange={handleChange}\n      />\n      <style jsx>{`\n        input {\n          outline: none;\n          width: 100%;\n          border: 1px solid var(--input-border-color);\n          background: var(--input-background-color);\n          color: var(--title-color);\n          border-radius: 3px;\n          box-sizing: border-box;\n          height: 32px;\n          padding: 4px 8px;\n          line-height: 32px;\n          font-size: 12px;\n          margin-top: 8px;\n          outline: none;\n          box-shadow: var(--input-shadow);\n        }\n\n        .has-errors {\n          background: rgba(255,59,48,0.10);\n          border-color: rgba(255,59,48,0.20);\n        }\n\n        input:focus {\n          border-color: var(--kap);\n        }\n\n        div {\n          width: 100%;\n        }\n      `}</style>\n    </div>\n  );\n};\n\nConfigInput.propTypes = {\n  name: PropTypes.string,\n  type: PropTypes.string,\n  schema: PropTypes.object,\n  value: PropTypes.oneOfType([\n    PropTypes.string,\n    PropTypes.bool,\n    PropTypes.number\n  ]),\n  onChange: PropTypes.elementType.isRequired,\n  hasErrors: PropTypes.bool\n};\n\nclass Tab extends React.Component {\n  render() {\n    const {validator, values, onChange, openConfig, viewOnGithub, serviceTitle} = this.props;\n\n    const {config, errors, description} = validator;\n    const allErrors = errors || [];\n\n    return (\n      <div className=\"container\">\n        {\n          description && (\n            <div className=\"description\">\n              <Linkify component={Link}>{description}</Linkify>\n            </div>\n          )\n        }\n        {\n          [...Object.keys(config)].map(key => {\n            const schema = config[key];\n            const type = schema.customType || (schema.enum ? 'select' : schema.type);\n            const itemErrors = allErrors\n              .filter(({dataPath}) => dataPath.endsWith(key))\n              .map(({message}) => `This ${message}`);\n\n            return (\n              <Item\n                key={key}\n                small\n                title={schema.title}\n                subtitle={schema.description}\n                vertical={!horizontalTypes.includes(type)}\n                errors={itemErrors}\n              >\n                <ConfigInput\n                  hasErrors={itemErrors.length > 0}\n                  name={key}\n                  type={type}\n                  schema={schema}\n                  value={values[key]}\n                  onChange={onChange}\n                />\n              </Item>\n            );\n          })\n        }\n        {\n          !serviceTitle && (\n            <Item subtitle=\"Open config file\" onClick={openConfig}>\n              <div className=\"icon-container\"><OpenConfigIcon fill=\"var(--kap)\" hoverFill=\"var(--kap)\" onClick={openConfig}/></div>\n            </Item>\n          )\n        }\n        <Item last subtitle=\"View plugin on GitHub\" onClick={viewOnGithub}>\n          <div className=\"icon-container\"><OpenOnGithubIcon size=\"20px\" fill=\"var(--kap)\" hoverFill=\"var(--kap)\" onClick={viewOnGithub}/></div>\n        </Item>\n        <style jsx>{`\n          .container {\n            width: 100%;\n            height: 100%;\n            overflow-y: auto;\n          }\n\n          .description {\n            color: var(--subtitle-color);\n            font-weight: normal;\n            font-size: 1.4rem;\n            width: 100%;\n            padding: 16px 16px 0 16px;\n            box-sizing: border-box;\n          }\n\n          .icon-container {\n            width: 24px;\n            height: 24px;\n            display: flex;\n            justify-content: center;\n            align-items: center;\n          }\n        `}</style>\n      </div>\n    );\n  }\n}\n\nTab.propTypes = {\n  validator: PropTypes.elementType,\n  values: PropTypes.object,\n  onChange: PropTypes.elementType.isRequired,\n  openConfig: PropTypes.elementType.isRequired,\n  viewOnGithub: PropTypes.elementType.isRequired,\n  serviceTitle: PropTypes.string\n};\n\nexport default Tab;\n"
  },
  {
    "path": "renderer/components/cropper/cursor.js",
    "content": "import electron from 'electron';\nimport PropTypes from 'prop-types';\nimport React from 'react';\nimport classNames from 'classnames';\n\nimport {connect, CursorContainer, CropperContainer} from '../../containers';\n\nclass Cursor extends React.Component {\n  remote = electron.remote || false;\n\n  render() {\n    if (!this.remote) {\n      return null;\n    }\n\n    const {\n      cursorY,\n      cursorX,\n      width,\n      height,\n      screenWidth,\n      screenHeight\n    } = this.props;\n\n    const className = classNames('dimensions', {\n      flipY: screenHeight - cursorY < 35,\n      flipX: screenWidth - cursorX < 40\n    });\n\n    return (\n      <div className={className}>\n        <div>{width}</div>\n        <div>{height}</div>\n        <style jsx>{`\n          .dimensions {\n            position: fixed;\n            top: ${cursorY}px;\n            left: ${cursorX}px;\n            padding: 10px;\n          }\n\n          .dimensions.flipX {\n            left: auto;\n            right: ${screenWidth - cursorX}px;\n          }\n\n          .dimensions.flipY {\n            top: auto;\n            bottom: ${screenHeight - cursorY}px;\n          }\n\n          .dimensions div {\n            font-size: 0.6rem;\n            text-shadow: 1px 1px 0 #fff\n          }\n        `}</style>\n      </div>\n    );\n  }\n}\n\nCursor.propTypes = {\n  cursorX: PropTypes.number,\n  cursorY: PropTypes.number,\n  width: PropTypes.number,\n  height: PropTypes.number,\n  screenWidth: PropTypes.number,\n  screenHeight: PropTypes.number\n};\n\nexport default connect(\n  [CursorContainer, CropperContainer],\n  ({cursorX, cursorY}, {screenWidth, screenHeight}) => ({cursorX, cursorY, screenWidth, screenHeight})\n)(Cursor);\n"
  },
  {
    "path": "renderer/components/cropper/handles.js",
    "content": "import React from 'react';\nimport classNames from 'classnames';\nimport PropTypes from 'prop-types';\n\nimport {connect, CropperContainer, ActionBarContainer} from '../../containers';\n\nclass Handle extends React.Component {\n  static defaultProps = {\n    size: 8,\n    top: false,\n    bottom: false,\n    left: false,\n    right: false,\n    ratioLocked: false\n  };\n\n  render() {\n    const {\n      size,\n      top,\n      bottom,\n      right,\n      left,\n      onClick,\n      ratioLocked\n    } = this.props;\n\n    const className = classNames('handle', {\n      'handle-top': top,\n      'handle-bottom': bottom,\n      'handle-right': right,\n      'handle-left': left,\n      'place-on-top': top + bottom + left + right === 2,\n      hide: ratioLocked && top + bottom + left + right === 1\n    });\n\n    return (\n      <div className={className} onMouseDown={() => onClick(this.props)}>\n        <style jsx>{`\n          .handle {\n            position: absolute;\n            width: ${size}px;\n            height: ${size}px;\n            border-radius: 50%;\n            background: white;\n            border: 1px solid gray;\n            top: calc(50% - ${size / 2}px);\n            left: calc(50% - ${size / 2}px);\n            z-index: 8;\n            ${getResizingCursor(this.props)}\n          }\n\n          .handle-top {\n            top: -${1 + (size / 2)}px;\n          }\n\n          .handle-bottom {\n            top: calc(100% - ${size / 2}px);\n          }\n\n          .handle-left {\n            left: -${1 + (size / 2)}px;\n          }\n\n          .handle-right {\n            left: calc(100% - ${size / 2}px);\n          }\n\n          .place-on-top {\n            z-index: 9;\n          }\n\n          .hide {\n            display: none;\n          }\n        `}</style>\n      </div>\n    );\n  }\n}\n\nHandle.propTypes = {\n  size: PropTypes.number,\n  top: PropTypes.bool,\n  bottom: PropTypes.bool,\n  left: PropTypes.bool,\n  right: PropTypes.bool,\n  onClick: PropTypes.elementType.isRequired,\n  ratioLocked: PropTypes.bool\n};\n\nclass Handles extends React.Component {\n  static defaultProps = {\n    ratioLocked: false,\n    width: 0,\n    height: 0\n  };\n\n  render() {\n    const {\n      startResizing,\n      showHandles,\n      ratioLocked,\n      width,\n      height,\n      isActive,\n      willStartRecording\n    } = this.props;\n\n    if (width + height === 0) {\n      return null;\n    }\n\n    const show = !willStartRecording && isActive && showHandles;\n\n    return (\n      <div className=\"content\">\n        <div className=\"border\">\n          {\n            show && [...(Array.from({length: 8}).keys())].map(\n              i => (\n                <Handle\n                  key={`handle-${i}`}\n                  border={1}\n                  top={i % 3 === 0}\n                  bottom={i % 3 === 1}\n                  left={Math.floor(i / 3) === 0}\n                  right={Math.floor(i / 3) === 1}\n                  ratioLocked={ratioLocked}\n                  onClick={startResizing}\n                />\n              )\n            )\n          }\n          { this.props.children }\n        </div>\n        <style jsx>{`\n          .border {\n            outline: 1px solid white;\n            position: relative;\n            flex: 1;\n            display: flex;\n          }\n\n          .border:before {\n            content: '';\n            position: absolute;\n            width: 100%;\n            height: 100%;\n            outline: 1px dashed black;\n            z-index: 2;\n          }\n        `}</style>\n      </div>\n    );\n  }\n}\n\nHandles.propTypes = {\n  isActive: PropTypes.bool,\n  width: PropTypes.number,\n  height: PropTypes.number,\n  startResizing: PropTypes.elementType.isRequired,\n  showHandles: PropTypes.bool,\n  ratioLocked: PropTypes.bool,\n  willStartRecording: PropTypes.bool,\n  children: PropTypes.oneOfType([\n    PropTypes.arrayOf(PropTypes.node),\n    PropTypes.node\n  ]).isRequired\n};\n\nexport default connect(\n  [CropperContainer, ActionBarContainer],\n  ({showHandles, width, height, isActive, willStartRecording}, {ratioLocked}) => ({showHandles, width, height, isActive, ratioLocked, willStartRecording}),\n  ({startResizing}) => ({startResizing})\n)(Handles);\n\nexport const getResizingCursor = ({top, bottom, right, left}) => {\n  if ((top || bottom) && !left && !right) {\n    return 'cursor: ns-resize;';\n  }\n\n  if ((left || right) && !top && !bottom) {\n    return 'cursor: ew-resize;';\n  }\n\n  if ((top && left) || (bottom && right)) {\n    return 'cursor: nwse-resize;';\n  }\n\n  if ((top && right) || (bottom && left)) {\n    return 'cursor: nesw-resize;';\n  }\n};\n"
  },
  {
    "path": "renderer/components/cropper/index.js",
    "content": "import PropTypes from 'prop-types';\nimport React from 'react';\n\nimport {connect, CropperContainer} from '../../containers';\n\nimport Handles from './handles';\nimport Cursor from './cursor';\n\nclass Cropper extends React.Component {\n  render() {\n    const {startMoving, width, height, isResizing} = this.props;\n\n    return (\n      <Handles>\n        <div\n          className=\"cropper\"\n          onMouseDown={startMoving}/>\n        { isResizing && <Cursor width={width} height={height}/> }\n        <style jsx>{`\n          .cropper {\n            flex: 1;\n            z-index: 6;\n          }\n        `}</style>\n      </Handles>\n    );\n  }\n}\n\nCropper.propTypes = {\n  startMoving: PropTypes.elementType.isRequired,\n  width: PropTypes.number,\n  height: PropTypes.number,\n  isResizing: PropTypes.bool\n};\n\nexport default connect(\n  [CropperContainer],\n  ({width, height, isResizing}) => ({width, height, isResizing}),\n  ({startMoving}) => ({startMoving})\n)(Cropper);\n"
  },
  {
    "path": "renderer/components/cropper/overlay.js",
    "content": "import classNames from 'classnames';\nimport PropTypes from 'prop-types';\nimport React from 'react';\n\nimport {\n  connect,\n  CursorContainer,\n  CropperContainer,\n  ActionBarContainer\n} from '../../containers';\n\nimport {getResizingCursor} from './handles';\n\nclass Overlay extends React.Component {\n  static defaultProps = {\n    x: 0,\n    y: 0,\n    width: 0,\n    height: 0,\n    isReady: false\n  };\n\n  render() {\n    const {\n      onMouseUp,\n      setCursor,\n      startPicking,\n      x,\n      y,\n      width,\n      height,\n      isMoving,\n      isResizing,\n      currentHandle,\n      isActive,\n      isReady,\n      screenWidth,\n      screenHeight,\n      isRecording,\n      selectedApp\n    } = this.props;\n\n    const contentClassName = classNames('content', {'not-ready': !isReady});\n\n    const className = classNames('overlay', {\n      recording: isRecording,\n      picking: !isRecording && !isResizing && !isMoving,\n      'no-transition': isResizing || isMoving || !isActive || Boolean(selectedApp)\n    });\n\n    return (\n      <div\n        className={contentClassName}\n        id=\"container\"\n        onMouseMove={setCursor}\n        onMouseUp={onMouseUp}\n      >\n        <div id=\"top\" className={className} onMouseDown={startPicking}/>\n        <div id=\"middle\">\n          <div id=\"left\" className={className} onMouseDown={startPicking}/>\n          <div id=\"center\">\n            { isReady && this.props.children }\n          </div>\n          <div id=\"right\" className={className} onMouseDown={startPicking}/>\n        </div>\n        <div id=\"bottom\" className={className} onMouseDown={startPicking}/>\n        <style jsx>{`\n          .overlay {\n            background-color: rgba(0, 0, 0, 0.5);\n            transition: background-color 0.5s ease-in-out, width 0.2s ease-out, height 0.2s ease-out;\n          }\n\n          .overlay.recording {\n            background-color: rgba(0, 0, 0, 0.1);\n          }\n\n          .overlay.picking {\n            cursor: crosshair;\n          }\n\n          .overlay.no-transition {\n            transition: background-color 0.5s ease-in-out;\n          }\n\n          #middle {\n            display: flex;\n            flex: 1;\n          }\n\n          #center {\n            flex: 1;\n            position: relative;\n            display: flex;\n          }\n\n          #left {\n            width: ${x}px;\n          }\n\n          #top {\n            height: ${y}px;\n          }\n\n          #right {\n            width: ${screenWidth - width - x}px;\n          }\n\n          #bottom {\n            height: ${screenHeight - height - y}px;\n          }\n\n          .not-ready .overlay {\n            background-color: rgba(0, 0, 0, 0);\n          }\n\n          .not-ready #left,\n          .not-ready #right {\n            width: 50%;\n          }\n\n          .not-ready #top,\n          .not-ready #bottom {\n            height: 50%;\n          }\n\n          #container {\n            flex-direction: column;\n            ${isMoving ? 'cursor: move;' : ''}\n            ${isResizing ? getResizingCursor(currentHandle) : ''}\n          }\n        `}</style>\n      </div>\n    );\n  }\n}\n\nOverlay.propTypes = {\n  onMouseUp: PropTypes.elementType.isRequired,\n  setCursor: PropTypes.elementType.isRequired,\n  startPicking: PropTypes.elementType.isRequired,\n  x: PropTypes.number,\n  y: PropTypes.number,\n  selectedApp: PropTypes.string,\n  width: PropTypes.number,\n  height: PropTypes.number,\n  isMoving: PropTypes.bool,\n  isResizing: PropTypes.bool,\n  currentHandle: PropTypes.object,\n  children: PropTypes.oneOfType([\n    PropTypes.arrayOf(PropTypes.node),\n    PropTypes.node\n  ]).isRequired,\n  isActive: PropTypes.bool,\n  isReady: PropTypes.bool,\n  screenWidth: PropTypes.number,\n  screenHeight: PropTypes.number,\n  isRecording: PropTypes.bool\n};\n\nexport default connect(\n  [CropperContainer, ActionBarContainer, CursorContainer],\n  ({x, y, width, height, isMoving, isResizing, currentHandle, screenWidth, screenHeight, isReady, isActive, isRecording, selectedApp}, actionBar) => ({\n    x, y, width, height, isResizing, currentHandle, screenWidth, screenHeight, isReady, isActive, isRecording, isMoving: isMoving || actionBar.isMoving, selectedApp\n  }),\n  ({stopMoving, stopResizing, stopPicking, startPicking}, actionBar, {setCursor}) => ({\n    onMouseUp: () => {\n      stopMoving();\n      stopResizing();\n      stopPicking();\n      actionBar.stopMoving();\n    },\n    setCursor,\n    startPicking\n  })\n)(Overlay);\n"
  },
  {
    "path": "renderer/components/dialog/actions.js",
    "content": "import React, {useState, useEffect, useRef} from 'react';\nimport PropTypes from 'prop-types';\n\nconst Actions = ({buttons, performAction, defaultId}) => {\n  const [activeButton, setActiveButton] = useState();\n  const defaultButton = useRef(null);\n\n  useEffect(() => {\n    setActiveButton();\n    if (defaultButton.current) {\n      defaultButton.current.focus();\n    }\n  }, [buttons]);\n\n  const action = async index => {\n    setActiveButton(index);\n    performAction(index);\n  };\n\n  return (\n    <div className=\"container\">\n      {\n        buttons.map((button, index) => (\n          <button\n            ref={index === defaultId ? defaultButton : undefined}\n            key={button.label}\n            type=\"button\"\n            disabled={index === activeButton}\n            onClick={async () => action(index)}\n          >\n            {index === activeButton ? button.activeLabel || button.label : button.label}\n          </button>\n        ))\n      }\n\n      <style jsx>{`\n        .container {\n          padding: 16px 24px 16px 0;\n          display: flex;\n          justify-content: flex-end;\n          flex-shrink: 0;\n        }\n\n        button {\n          white-space: nowrap;\n          padding: 4px 20px;\n          font-size: 1.25rem;\n          color: var(--title-color);\n          border-radius: 4px;\n          text-align: center;\n        }\n\n        button:disabled {\n          opacity: 0.5;\n        }\n\n        button + button {\n          margin-left: 16px;\n        }\n      `}</style>\n    </div>\n  );\n};\n\nActions.propTypes = {\n  performAction: PropTypes.elementType,\n  defaultId: PropTypes.number,\n  buttons: PropTypes.arrayOf(PropTypes.object)\n};\n\nexport default Actions;\n"
  },
  {
    "path": "renderer/components/dialog/body.js",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\n\nconst Body = ({title, message, detail}) => {\n  return (\n    <div className=\"container\">\n      <h1>{title}</h1>\n      <div className=\"detail\">\n        {\n          detail.split('\\n').map(text => (\n            <span key={text}>{text}</span>\n          ))\n        }\n      </div>\n      {message && <p>{message}</p>}\n\n      <style jsx>{`\n        h1 {\n          font-size: 1.25rem;\n          margin: 0;\n          color: var(--title-color);\n        }\n\n        .detail {\n          margin-top: 8px;\n          font-size: 1.125rem;\n          max-height: 400px;\n          overflow-y: scroll;\n          display: flex;\n          flex-direction: column;\n          color: var(--title-color);\n          flex: 1;\n        }\n\n        span {\n          min-height: 1.125rem;\n        }\n\n        p {\n          font-size: 1.25rem;\n          color: var(--title-color);\n          margin: 24px 0 0 0;\n          white-space: nowrap;\n        }\n\n        .container {\n          flex-direction: column;\n          display: flex;\n          padding: 24px 24px 0 0;\n          flex: 1;\n        }\n      `}</style>\n    </div>\n  );\n};\n\nBody.propTypes = {\n  title: PropTypes.string,\n  message: PropTypes.string,\n  detail: PropTypes.string\n};\n\nexport default Body;\n"
  },
  {
    "path": "renderer/components/dialog/icon.js",
    "content": "import React from 'react';\n\nconst Icon = () => {\n  return (\n    <div>\n      <img src=\"/static/kap-icon.png\"/>\n      <style jsx>{`\n        img {\n          width: 58px;\n          height: 58px;\n        }\n\n        div {\n          padding: 24px;\n          pointer-events: none;\n        }\n      `}</style>\n    </div>\n  );\n};\n\nexport default Icon;\n"
  },
  {
    "path": "renderer/components/editor/controls/left.tsx",
    "content": "import VideoControlsContainer from '../video-controls-container';\nimport VideoTimeContainer from '../video-time-container';\nimport {PlayIcon, PauseIcon} from '../../../vectors';\nimport formatTime from '../../../utils/format-time';\n\nconst LeftControls = () => {\n  const {isPaused, play, pause} = VideoControlsContainer.useContainer();\n  const {currentTime} = VideoTimeContainer.useContainer();\n\n  return (\n    <div className=\"container\">\n      <div className=\"play\">\n        {\n          isPaused ?\n            <PlayIcon shadow size=\"26px\" fill=\"#fff\" hoverFill=\"#fff\" onClick={play}/> :\n            <PauseIcon shadow size=\"26px\" fill=\"#fff\" hoverFill=\"#fff\" onClick={pause}/>\n        }\n      </div>\n      <div className=\"time\">{formatTime(currentTime, {showMilliseconds: false})}</div>\n      <style jsx>{`\n            .container {\n              display: flex;\n              color: white;\n              width: 100%;\n              font-size: 12px;\n              align-items: center;\n              padding: 0 16px;\n            }\n\n            .play {\n              width: 26px;\n              height: 26px;\n              margin-right: 16px;\n              display: flex;\n              align-items: center;\n            }\n\n            .time {\n              width: 46px;\n              text-shadow: 1px 1px rgba(0, 0, 0, 0.1);\n            }\n        `}</style>\n    </div>\n  );\n};\n\nexport default LeftControls;\n"
  },
  {
    "path": "renderer/components/editor/controls/play-bar.tsx",
    "content": "import VideoTimeContainer from '../video-time-container';\nimport {useState, useRef} from 'react';\nimport VideoControlsContainer from '../video-controls-container';\nimport Preview from './preview';\n\nconst PlayBar = () => {\n  const [resizing, setResizing] = useState(false);\n  const [hoverTime, setHoverTime] = useState(0);\n  const progress = useRef<HTMLProgressElement>();\n\n  const {play, pause} = VideoControlsContainer.useContainer();\n  const {\n    currentTime,\n    duration,\n    startTime,\n    endTime,\n    updateTime,\n    updateStartTime,\n    updateEndTime\n  } = VideoTimeContainer.useContainer();\n\n  const total = endTime - startTime;\n  const current = currentTime - startTime;\n\n  const getTimeFromEvent = event => {\n    const cursorX = event.clientX;\n    const {x, width} = progress.current.getBoundingClientRect();\n\n    const percent = (cursorX - x) / width;\n    const time = startTime + ((endTime - startTime) * percent);\n\n    return Math.max(0, time);\n  };\n\n  const seek = event => {\n    const time = getTimeFromEvent(event);\n\n    if (startTime <= time && time <= endTime) {\n      updateTime(time);\n    }\n  };\n\n  const updatePreview = event => {\n    setHoverTime(getTimeFromEvent(event));\n  };\n\n  const startResizing = () => {\n    setResizing(true);\n    pause();\n  };\n\n  const stopResizing = () => {\n    setResizing(false);\n    play();\n  };\n\n  const setStartTime = event => {\n    updateStartTime(Number.parseFloat(event.target.value));\n  };\n\n  const setEndTime = event => {\n    updateEndTime(Number.parseFloat(event.target.value));\n  };\n\n  const previewTime = resizing ? currentTime : hoverTime;\n  const previewLabelTime = resizing ? currentTime : (startTime <= hoverTime && hoverTime <= endTime ? hoverTime - startTime : hoverTime);\n  const previewDuration = resizing ? total : (startTime <= hoverTime && hoverTime <= endTime ? total : undefined);\n\n  return (\n    <div className=\"container\" onMouseUp={seek} onMouseMove={updatePreview}>\n      <div className=\"progress-bar-container\">\n        <div className=\"progress-bar\">\n          <progress ref={progress} max={total} value={current}/>\n          <div className=\"preview\">\n            <Preview time={previewTime} labelTime={previewLabelTime} duration={previewDuration} hidePreview={resizing}/>\n          </div>\n          <input\n            type=\"range\"\n            className=\"slider start\"\n            value={startTime}\n            min={0}\n            max={duration}\n            step={0.00001}\n            onChange={setStartTime}\n            onMouseDown={startResizing}\n            onMouseUp={stopResizing}/>\n          <input\n            type=\"range\"\n            className=\"slider end\"\n            value={endTime}\n            min={0}\n            max={duration}\n            step={0.00001}\n            onChange={setEndTime}\n            onMouseDown={startResizing}\n            onMouseUp={stopResizing}/>\n        </div>\n      </div>\n      <style jsx>{`\n            .container {\n              flex: 1;\n              display: flex;\n              align-items: center;\n              z-index: 25;\n              overflow: visible;\n              height: 50%;\n            }\n\n            .progress-bar-container {\n              position: absolute;\n              width: 100%;\n              display: flex;\n              bottom: 30px;\n              left: 50%;\n              transform: translateX(-50%);\n              width: 60%;\n              transition: all 0.12s ease-in-out;\n            }\n\n            .progress-bar {\n              width: 100%;\n              height: 4px;\n              display: flex;\n              background: rgba(255, 255, 255, 0.2);\n              border-radius: 4px;\n              position: relative;\n            }\n\n            progress {\n              position: absolute;\n              top: 0;\n              width: ${total * 100 / duration}%;\n              left: ${startTime * 100 / duration}%;\n              -webkit-appearance: none;\n              height: 4px;\n              border-radius: 4px;\n            }\n\n            progress::-webkit-progress-bar {\n              background-color: rgba(255, 255, 255, 0.4);\n              border-radius: 4px;\n            }\n\n            progress::-webkit-progress-value {\n              border-radius: 4px;\n              background-image: linear-gradient(90deg, #9300ff 0%, #5272e2 49%, #05e6b5 98%);\n              box-shadow: inset 0 0 0 0.5px rgba(255, 255, 255, 0.1);\n            }\n\n            .slider {\n              width: 100%;\n              height: 4px;\n              position: absolute;\n              margin: 0;\n              top: 0;\n              -webkit-appearance: none;\n              outline: none;\n              background: transparent;\n              pointer-events: none;\n            }\n\n            .slider::-ms-track {\n              width: 100%;\n              height: 0;\n              border-color: transparent;\n              color: transparent;\n              background: transparent;\n              pointer-events: none;\n              z-index: -1;\n            }\n\n            .slider::-webkit-slider-thumb {\n              width: 5px;\n              height: 16px;\n              background: #fff;\n              border-radius: 2px;\n              box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);\n              transition: all 0.16s ease-in-out;\n              -webkit-appearance: none;\n              pointer-events: auto;\n              z-index: 20;\n            }\n\n            .preview {\n              position: absolute;\n              left: ${hoverTime * 100 / duration}%;\n              transform: translateX(-50%);\n              bottom: 20px;\n              width: 132px;\n              height: 88px;\n              display: none;\n            }\n\n            .container:hover .preview {\n              display: flex;\n            }\n        `}</style>\n    </div>\n  );\n};\n\nexport default PlayBar;\n\n// Import PropTypes from 'prop-types';\n// import React from 'react';\n// import classNames from 'classnames';\n\n// import {connect, VideoContainer} from '../../../containers';\n// import Preview from './preview';\n\n// class PlayBar extends React.Component {\n//   state = {\n//     hoverTime: 0\n//   };\n\n//   progress = React.createRef();\n\n// getTimeFromEvent = event => {\n//   const {startTime, endTime} = this.props;\n\n//   const cursorX = event.clientX;\n//   const {x, width} = this.progress.current.getBoundingClientRect();\n\n//   const percent = (cursorX - x) / width;\n//   const time = startTime + ((endTime - startTime) * percent);\n\n//   return Math.max(0, time);\n// }\n\n// seek = event => {\n//   const {startTime, endTime, seek} = this.props;\n//   const time = this.getTimeFromEvent(event);\n\n//   if (startTime <= time && time <= endTime) {\n//     seek(time);\n//   }\n// }\n\n// updatePreview = event => {\n//   const time = this.getTimeFromEvent(event);\n//   this.setState({hoverTime: time});\n// }\n\n// startResizing = () => {\n//   const {pause} = this.props;\n//   this.setState({resizing: true});\n//   pause();\n// }\n\n// stopResizing = () => {\n//   const {play} = this.props;\n//   this.setState({resizing: false});\n//   play();\n// }\n\n// setStartTime = event => this.props.setStartTime(Number.parseFloat(event.target.value))\n\n// setEndTime = event => this.props.setEndTime(Number.parseFloat(event.target.value))\n\n//   render() {\n// const {currentTime = 0, duration, startTime, endTime, hover, src} = this.props;\n\n// if (!src) {\n//   return null;\n// }\n\n// const {hoverTime, resizing} = this.state;\n\n// const total = endTime - startTime;\n// const current = currentTime - startTime;\n\n// const previewTime = resizing ? currentTime : hoverTime;\n// const previewLabelTime = resizing ? currentTime : (startTime <= hoverTime && hoverTime <= endTime ? hoverTime - startTime : hoverTime);\n// const previewDuration = resizing ? total : (startTime <= hoverTime && hoverTime <= endTime ? total : undefined);\n\n// const className = classNames('progress-bar-container', {hover});\n\n// return (\n//   <div className=\"container\" onMouseUp={this.seek} onMouseMove={this.updatePreview}>\n//     <div className={className}>\n//       <div className=\"progress-bar\">\n//         <progress ref={this.progress} max={total} value={current}/>\n//         <div className=\"preview\">\n//           <Preview src={src} time={previewTime} labelTime={previewLabelTime} duration={previewDuration} hidePreview={resizing}/>\n//         </div>\n//         <input\n//           type=\"range\"\n//           className=\"slider start\"\n//           value={startTime}\n//           min={0}\n//           max={duration}\n//           step={0.00001}\n//           onChange={this.setStartTime}\n//           onMouseDown={this.startResizing}\n//           onMouseUp={this.stopResizing}/>\n//         <input\n//           type=\"range\"\n//           className=\"slider end\"\n//           value={endTime}\n//           min={0}\n//           max={duration}\n//           step={0.00001}\n//           onChange={this.setEndTime}\n//           onMouseDown={this.startResizing}\n//           onMouseUp={this.stopResizing}/>\n//       </div>\n//     </div>\n//     <style jsx>{`\n//         .container {\n//           flex: 1;\n//           display: flex;\n//           align-items: center;\n//           z-index: 25;\n//           overflow: visible;\n//           height: 50%;\n//         }\n\n//         .progress-bar-container {\n//           position: absolute;\n//           width: 100%;\n//           display: flex;\n//           bottom: 30px;\n//           left: 50%;\n//           transform: translateX(-50%);\n//           width: 60%;\n//           transition: all 0.12s ease-in-out;\n//         }\n\n//         .progress-bar-container:not(.hover) {\n//           bottom: 64px;\n//           width: 100%\n//         }\n\n//         .progress-bar-container:not(.hover) .progress-bar {\n//           border-radius: 0;\n//         }\n\n//         .progress-bar {\n//           width: 100%;\n//           height: 4px;\n//           display: flex;\n//           background: rgba(255, 255, 255, 0.2);\n//           border-radius: 4px;\n//           position: relative;\n//         }\n\n//         progress {\n//           position: absolute;\n//           top: 0;\n//           width: ${total * 100 / duration}%;\n//           left: ${startTime * 100 / duration}%;\n//           -webkit-appearance: none;\n//           height: 4px;\n//           border-radius: 4px;\n//         }\n\n//         progress::-webkit-progress-bar {\n//           background-color: rgba(255, 255, 255, 0.4);\n//           border-radius: 4px;\n//         }\n\n//         progress::-webkit-progress-value {\n//           border-radius: 4px;\n//           background-image: linear-gradient(90deg, #9300ff 0%, #5272e2 49%, #05e6b5 98%);\n//           box-shadow: inset 0 0 0 0.5px rgba(255, 255, 255, 0.1);\n//         }\n\n//         .slider {\n//           width: 100%;\n//           height: 4px;\n//           position: absolute;\n//           margin: 0;\n//           top: 0;\n//           -webkit-appearance: none;\n//           outline: none;\n//           background: transparent;\n//           pointer-events: none;\n//           ${hover ? '' : 'display: none;'}\n//         }\n\n//         .slider::-ms-track {\n//           width: 100%;\n//           height: 0;\n//           border-color: transparent;\n//           color: transparent;\n//           background: transparent;\n//           pointer-events: none;\n//           z-index: -1;\n//         }\n\n//         .slider::-webkit-slider-thumb {\n//           width: 5px;\n//           height: 16px;\n//           background: #fff;\n//           border-radius: 2px;\n//           box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);\n//           transition: all 0.16s ease-in-out;\n//           -webkit-appearance: none;\n//           pointer-events: auto;\n//           z-index: 20;\n//         }\n\n//         .preview {\n//           position: absolute;\n//           left: ${hoverTime * 100 / duration}%;\n//           transform: translateX(-50%);\n//           bottom: 20px;\n//           width: 132px;\n//           height: 88px;\n//           display: none;\n//         }\n\n//         .container:hover .preview {\n//           display: flex;\n//         }\n//     `}</style>\n//   </div>\n// );\n//   }\n// }\n\n// PlayBar.propTypes = {\n//   startTime: PropTypes.number,\n//   endTime: PropTypes.number,\n//   seek: PropTypes.elementType,\n//   currentTime: PropTypes.number,\n//   duration: PropTypes.number,\n//   src: PropTypes.string,\n//   setStartTime: PropTypes.elementType,\n//   setEndTime: PropTypes.elementType,\n//   pause: PropTypes.elementType,\n//   play: PropTypes.elementType,\n//   hover: PropTypes.bool\n// };\n\n// export default connect(\n//   [VideoContainer],\n//   ({currentTime, duration, startTime, endTime, src}) => ({currentTime, duration, startTime, endTime, src}),\n//   ({seek, setStartTime, setEndTime, pause, play}) => ({seek, setStartTime, setEndTime, pause, play})\n// )(PlayBar);\n"
  },
  {
    "path": "renderer/components/editor/controls/preview.tsx",
    "content": "import formatTime from '../../../utils/format-time';\nimport {useRef, useEffect} from 'react';\nimport useEditorWindowState from 'hooks/editor/use-editor-window-state';\n\ntype Props = {\n  time: number;\n  labelTime: number;\n  duration: number;\n  hidePreview: boolean;\n};\n\nconst Preview = ({time, labelTime, duration, hidePreview}: Props) => {\n  const videoRef = useRef<HTMLVideoElement>();\n  const {filePath} = useEditorWindowState();\n  const src = `file://${filePath}`;\n\n  useEffect(() => {\n    if (!hidePreview) {\n      videoRef.current.currentTime = time;\n    }\n  }, [time, hidePreview]);\n\n  return (\n    <div\n      className=\"container\" onMouseMove={event => {\n        event.stopPropagation();\n      }}\n    >\n      <video ref={videoRef} preload=\"auto\" src={src}/>\n      <div className=\"time\">{formatTime(labelTime, {extra: duration})}</div>\n      <style jsx>{`\n          .container {\n            flex: 1;\n            position: relative;\n          }\n\n          .time {\n            position: absolute;\n            bottom: 8px;\n            left: 50%;\n            transform: translateX(-50%);\n            width: max-content;\n            height: 24px;\n            background: rgba(0, 0, 0, 0.4);\n            color: #fff;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n            border-radius: 4px;\n            font-size: 12px;\n            padding: 4px 8px;\n          }\n\n          video {\n            width: 100%;\n            height: 100%;\n            border-radius: 4px;\n            box-shadow: 0px 0px 16px rgba(0, 0, 0, 0.1);\n            ${hidePreview ? 'display: none;' : ''}\n          }\n        `}</style>\n    </div>\n  );\n};\n\nexport default Preview;\n\n// Import PropTypes from 'prop-types';\n// import React from 'react';\n\n// import formatTime from '../../../utils/format-time';\n\n// class Preview extends React.Component {\n//   constructor(props) {\n//     super(props);\n//     this.videoRef = React.createRef();\n//   }\n\n//   shouldComponentUpdate(nextProps) {\n//     return nextProps.time !== this.props.time || nextProps.hidePreview !== this.props.hidePreview;\n//   }\n\n//   componentDidUpdate(previousProps) {\n//     if (previousProps.time !== this.props.time) {\n//       this.videoRef.current.currentTime = this.props.time;\n//     }\n//   }\n\n//   render() {\n//     const {labelTime, duration, hidePreview, src} = this.props;\n\n// return (\n//   <div className=\"container\" onMouseMove={event => event.stopPropagation()}>\n//     <video ref={this.videoRef} preload=\"auto\" src={src}/>\n//     <div className=\"time\">{formatTime(labelTime, {extra: duration})}</div>\n//     <style jsx>{`\n//       .container {\n//         flex: 1;\n//         position: relative;\n//       }\n\n//       .time {\n//         position: absolute;\n//         bottom: 8px;\n//         left: 50%;\n//         transform: translateX(-50%);\n//         width: max-content;\n//         height: 24px;\n//         background: rgba(0, 0, 0, 0.4);\n//         color: #fff;\n//         display: flex;\n//         align-items: center;\n//         justify-content: center;\n//         border-radius: 4px;\n//         font-size: 12px;\n//         padding: 4px 8px;\n//       }\n\n//       video {\n//         width: 100%;\n//         height: 100%;\n//         border-radius: 4px;\n//         box-shadow: 0px 0px 16px rgba(0, 0, 0, 0.1);\n//         ${hidePreview ? 'display: none;' : ''}\n//       }\n//     `}</style>\n//   </div>\n// );\n//   }\n// }\n\n// Preview.propTypes = {\n//   time: PropTypes.number,\n//   labelTime: PropTypes.number,\n//   duration: PropTypes.number,\n//   hidePreview: PropTypes.bool,\n//   src: PropTypes.string\n// };\n\n// export default Preview;\n"
  },
  {
    "path": "renderer/components/editor/controls/right.tsx",
    "content": "import {VolumeHighIcon, VolumeOffIcon} from '../../../vectors';\nimport VideoControlsContainer from '../video-controls-container';\nimport VideoMetadataContainer from '../video-metadata-container';\n\nimport formatTime from '../../../utils/format-time';\n\nconst RightControls = () => {\n  const {isMuted, mute, unmute} = VideoControlsContainer.useContainer();\n  const {hasAudio, duration} = VideoMetadataContainer.useContainer();\n\n  // FIXME\n  const format = 'mp4';\n\n  const canUnmute = !['gif', 'apng'].includes(format) && hasAudio;\n  const unmuteColor = canUnmute ? '#fff' : 'rgba(255, 255, 255, 0.40)';\n\n  return (\n    <div className=\"container\">\n      <div className=\"time\">{formatTime(duration)}</div>\n      <div className=\"mute\">\n        {\n          isMuted || !hasAudio ?\n            <VolumeOffIcon shadow fill={unmuteColor} hoverFill={unmuteColor} tabIndex={canUnmute ? undefined : -1} onClick={canUnmute ? unmute : undefined}/> :\n            <VolumeHighIcon shadow fill=\"#fff\" hoverFill=\"#fff\" onClick={mute}/>\n        }\n      </div>\n      <style jsx>{`\n            .container {\n              display: flex;\n              width: 100%;\n              align-items: center;\n              font-size: 12px;\n              padding: 0 16px;\n              color: white;\n              justify-content: flex-end;\n            }\n\n            .mute {\n              width: 24px;\n              height: 24px;\n              margin-left: 16px;\n            }\n\n            .time {\n              text-shadow: 1px 1px rgba(0, 0, 0, 0.1);\n              text-align: left;\n              width: 46px;\n            }\n        `}</style>\n    </div>\n  );\n};\n\nexport default RightControls;\n"
  },
  {
    "path": "renderer/components/editor/conversion/conversion-details.tsx",
    "content": "import {UseConversionState} from 'hooks/editor/use-conversion';\n\nconst ConversionDetails = ({conversion, showInFolder}: {conversion: UseConversionState; showInFolder: () => void}) => {\n  const message = conversion?.message;\n  const title = conversion?.titleWithFormat;\n  const description = conversion?.description;\n  const size = conversion?.fileSize;\n\n  return (\n    <div className=\"conversion-details\">\n      <div className=\"message\">{message}</div>\n      <div className=\"details\">\n        <div className=\"left\">\n          <div className=\"title\" title={title} onClick={showInFolder}>{title}</div>\n          <div className=\"description\">{description}</div>\n        </div>\n        <div className=\"size\">{size}</div>\n      </div>\n      <style jsx>{`\n        .conversion-details {\n          display: flex;\n          height: fit-content;\n          width: 100%;\n          padding: 24px;\n          flex-direction: column;\n          color: var(--white);\n          flex-shrink: 0;\n          line-height: 16px;\n        }\n\n        .message {\n          padding-bottom: 24px;\n          border-bottom: 1px solid #404040;\n          color: #aaaaaa;\n          font-size: 14px;\n        }\n\n        .details {\n          padding-top: 24px;\n          display: flex;\n          line-height: 20px;\n        }\n\n        .left {\n          flex: 1;\n          display: flex;\n          flex-direction: column;\n          overflow: hidden;\n        }\n\n        .title {\n          font-weight: 500;\n          font-size: 14px;\n          white-space: nowrap;\n          overflow: hidden;\n          text-overflow: ellipsis;\n        }\n\n        .description {\n          color: #aaaaaa;\n          font-size: 12px;\n        }\n\n        .size {\n          font-size: 14px;\n          flex-shrink: 0;\n          margin-left: 8px;\n        }\n      `}</style>\n    </div>\n  );\n};\n\nexport default ConversionDetails;\n"
  },
  {
    "path": "renderer/components/editor/conversion/index.tsx",
    "content": "import {ExportStatus} from 'common/types';\nimport useConversion from 'hooks/editor/use-conversion';\nimport useConversionIdContext from 'hooks/editor/use-conversion-id';\nimport {useConfirmation} from 'hooks/use-confirmation';\nimport {useMemo} from 'react';\nimport {useKeyboardAction} from '../../../hooks/use-keyboard-action';\nimport ConversionDetails from './conversion-details';\nimport TitleBar from './title-bar';\nimport VideoPreview from './video-preview';\n\nconst dialogOptions = {\n  message: 'Are you sure you want to discard this conversion?',\n  detail: 'Any progress will be lost.',\n  confirmButtonText: 'Discard'\n};\n\nconst EditorConversionView = ({conversionId}: {conversionId: string}) => {\n  const {setConversionId} = useConversionIdContext();\n  const conversion = useConversion(conversionId);\n\n  const inProgress = conversion.state?.status === ExportStatus.inProgress;\n\n  const cancel = () => {\n    if (inProgress) {\n      conversion.cancel();\n    }\n  };\n\n  const safeCancel = useConfirmation(cancel, dialogOptions);\n\n  const cancelAndGoBack = () => {\n    cancel();\n    setConversionId('');\n  };\n\n  const finalCancel = useMemo(() => inProgress ? safeCancel : () => { /* do nothing */ }, [inProgress]);\n\n  useKeyboardAction('Escape', finalCancel);\n\n  const showInFolder = () => conversion.showInFolder();\n\n  return (\n    <div className=\"editor-conversion-view\">\n      <TitleBar\n        conversion={conversion.state}\n        cancel={cancelAndGoBack}\n        copy={() => {\n          conversion.copy();\n        }}\n        retry={() => {\n          conversion.retry();\n        }}\n        showInFolder={showInFolder}/>\n      <VideoPreview conversion={conversion.state} cancel={finalCancel} showInFolder={showInFolder}/>\n      <ConversionDetails conversion={conversion.state} showInFolder={showInFolder}/>\n      <style jsx>{`\n        .editor-conversion-view {\n          width: 370px;\n          display: flex;\n          flex-direction: column;\n          flex: 1;\n          -webkit-app-region: no-drag;\n        }\n      `}</style>\n    </div>\n  );\n};\n\nexport default EditorConversionView;\n"
  },
  {
    "path": "renderer/components/editor/conversion/title-bar.tsx",
    "content": "import TrafficLights from 'components/traffic-lights';\nimport {BackPlainIcon, MoreIcon} from 'vectors';\nimport {UseConversionState} from 'hooks/editor/use-conversion';\nimport {flags} from '../../../common/flags';\nimport {MenuItemConstructorOptions, remote} from 'electron';\nimport {ExportStatus} from '../../../common/types';\nimport {useMemo} from 'react';\nimport {template} from 'lodash';\nimport IconMenu from '../../icon-menu';\n\nconst TitleBar = ({conversion, cancel, copy, retry, showInFolder}: {conversion: UseConversionState; cancel: () => any; copy: () => any; retry: () => any; showInFolder: () => void}) => {\n  const {api} = require('electron-util');\n  const shouldClose = async () => {\n    if (conversion.status === ExportStatus.inProgress && !flags.get('backgroundEditorConversion')) {\n      await api.dialog.showMessageBox(remote.getCurrentWindow(), {\n        type: 'info',\n        message: 'Your export will continue in the background. You can access it through the Export History window.',\n        buttons: ['Ok'],\n        defaultId: 0\n      });\n      flags.set('backgroundEditorConversion', true);\n    }\n\n    return true;\n  };\n\n  const menuTemplate = useMemo(() => {\n    const template: MenuItemConstructorOptions[] = [];\n\n    if (conversion?.canCopy) {\n      template.push({\n        label: 'Copy',\n        click: () => copy()\n      }, {\n        type: 'separator'\n      });\n    }\n\n    if (conversion?.status === ExportStatus.completed) {\n      template.push({\n        label: 'Show in Finder',\n        click: () => showInFolder()\n      });\n    }\n\n    return template;\n  }, [conversion?.canCopy, conversion?.status]);\n\n  const canRetry = [ExportStatus.canceled, ExportStatus.failed].includes(conversion?.status);\n\n  return (\n    <div className=\"title-bar\">\n      <div className=\"left\">\n        <TrafficLights shouldClose={shouldClose}/>\n        <div className=\"icon\" onClick={cancel}>\n          <BackPlainIcon fill=\"white\" hoverFill=\"white\" size=\"100%\"/>\n        </div>\n      </div>\n      <div className=\"right\">\n        {canRetry && <div className=\"button\" onClick={retry}>Retry</div>}\n        {\n          menuTemplate.length > 0 && (\n            <div className=\"icon\">\n              <IconMenu\n                icon={MoreIcon}\n                fill=\"white\"\n                hoverFill=\"white\"\n                activeFill=\"white\"\n                size=\"20px\"\n                template={menuTemplate}\n              />\n            </div>\n          )\n        }\n      </div>\n      <style jsx>{`\n        .title-bar {\n          height: 36px;\n          padding: 6px 12px 6px 0px;\n          display: flex;\n          align-items: center;\n          justify-content: space-between;\n          -webkit-app-region: drag;\n        }\n\n        .left {\n          display: flex;\n          align-items: center;\n          height: 100%;\n        }\n\n        .right {\n          display: flex;\n          height: 100%;\n        }\n\n        .icon {\n          width: 24px;\n          height: 24px;\n          background: #666666;\n          border-radius: 4px;\n          display: flex;\n          align-items: center;\n          justify-content: center;\n        }\n\n        .button {\n          height: 24px;\n          color: white;\n          padding: 4px 8px;\n          background: #666666;\n          border-radius: 4px;\n          display: flex;\n          align-items: center;\n          justify-content: center;\n          font-size: 12px;\n          line-height: 16px;\n          font-weight: 500;\n        }\n\n        .button:active,\n        .icon:active {\n          background: hsla(0, 0%, 100%, 0.2);\n          outline: none;\n        }\n      `}</style>\n    </div>\n  );\n};\n\nexport default TitleBar;\n"
  },
  {
    "path": "renderer/components/editor/conversion/video-preview.tsx",
    "content": "import {CancelIcon, SpinnerIcon} from 'vectors';\nimport {UseConversion, UseConversionState} from 'hooks/editor/use-conversion';\nimport {ExportStatus} from 'common/types';\nimport useEditorWindowState from 'hooks/editor/use-editor-window-state';\nimport useConversionIdContext from 'hooks/editor/use-conversion-id';\nimport {flags} from '../../../common/flags';\nimport ReactTooltip from 'react-tooltip';\nimport {useEffect, useRef, useState} from 'react';\nimport classNames from 'classnames';\n\nconst VideoPreview = ({conversion, cancel, showInFolder}: {conversion: UseConversionState; cancel: () => any; showInFolder: () => any}) => {\n  const {conversionId} = useConversionIdContext();\n  const {filePath} = useEditorWindowState();\n  const [tooltipShowing, setTooltipShowing] = useState(!flags.get('editorDragTooltip'));\n  const tooltipRef = useRef();\n  const src = `file://${filePath}`;\n\n  const percentage = conversion?.progress ?? 0;\n  const done = conversion && (conversion?.status !== ExportStatus.inProgress);\n\n  const onDragStart = (event: any) => {\n    event.preventDefault();\n    // Has to be the electron one for this\n    const {ipcRenderer} = require('electron');\n    ipcRenderer.send('drag-export', conversionId);\n  };\n\n  useEffect(() => {\n    if (!done) {\n      return;\n    }\n\n    if (tooltipShowing) {\n      ReactTooltip.show(tooltipRef.current);\n    } else {\n      ReactTooltip.hide(tooltipRef.current);\n    }\n  }, [tooltipRef.current, tooltipShowing, done]);\n\n  const onTooltipClick = event => {\n    event.stopPropagation();\n    setTooltipShowing(false);\n  };\n\n  const onTooltipHide = () => {\n    flags.set('editorDragTooltip', true);\n  };\n\n  return (\n    <div\n      ref={tooltipRef}\n      data-tip=\"Plz\"\n      draggable={done}\n      className={classNames('video-preview', {'hide-tooltip': !tooltipShowing})}\n      data-for=\"tooltip\"\n      onDragStart={onDragStart}\n      onClick={showInFolder}\n    >\n      {\n        done && conversion?.canPreviewExport ?\n          <img src={`file://${conversion?.filePath}`}/> :\n          <video src={src}/>\n      }\n      <div className=\"overlay\" style={{display: done ? 'none' : 'flex'}}>\n        <div className=\"progress-indicator\">\n          {\n            percentage === 0 ?\n              <IndeterminateSpinner/> :\n              <ProgressCircle percent={percentage}/>\n          }\n        </div>\n        <div className=\"cancel\" title=\"Cancel\" onClick={cancel}>\n          <CancelIcon fill=\"white\" hoverFill=\"white\" activeFill=\"white\" size=\"100%\"/>\n        </div>\n      </div>\n      <ReactTooltip\n        border\n        multiline\n        clickable\n        disable={!tooltipShowing}\n        place=\"bottom\"\n        event=\"dblclick\"\n        eventOff=\"dblclick\"\n        className=\"tooltip\"\n        id=\"tooltip\"\n        backgroundColor=\"var(--background-color)\"\n        effect=\"solid\"\n        borderColor=\"rgba(255, 255, 255, 0.4)\"\n        afterHide={onTooltipHide}\n      >\n        <div className=\"tooltip-content\" onClick={onTooltipClick}>Drag and drop to copy the recording to your desktop or an application. Click to open its parent directory</div>\n      </ReactTooltip>\n      <style jsx>{`\n        .video-preview {\n          width: 100%;\n          height: wrap-content;\n          background: black;\n          position: relative;\n          flex: 1;\n          height: 0;\n          -webkit-app-region: no-drag;\n          max-height: 200px;\n        }\n\n        video, img {\n          width: 100%;\n          height: 100%;\n        }\n\n        img {\n          object-fit: contain;\n        }\n\n        .overlay {\n          width: 100%;\n          height: 100%;\n          position: absolute;\n          top: 0;\n          left: 0;\n          display: flex;\n          align-items: center;\n          justify-content: center;\n          background: rgba(0, 0, 0, .5);\n        }\n\n        .progress-indicator, .cancel {\n          width: 48px;\n          height: 48px;\n          position: absolute;\n          transition: opacity 0.35s ease-in-out;\n        }\n\n        .cancel {\n          width: 24px;\n          height: 24px;\n          pointer-events: none;\n          opacity: 0;\n        }\n\n        .overlay:hover .cancel {\n          opacity: 1;\n          pointer-events: auto;\n        }\n      `}</style>\n    </div>\n  );\n};\n\nconst IndeterminateSpinner = () => (\n  <div className=\"container\">\n    <SpinnerIcon stroke=\"#fff\"/>\n    <style jsx>{`\n          .container {\n            width: 100%;\n            height: 100%;\n            animation: spin 3s linear infinite;\n          }\n\n          @keyframes spin {\n            0% {\n              transform: rotate(0deg);\n            }\n\n            50% {\n              transform: rotate(720deg);\n            }\n\n            100% {\n              transform: rotate(1080deg);\n            }\n          }\n        `}</style>\n  </div>\n);\n\nconst ProgressCircle = ({percent}: {percent: number}) => {\n  const circumference = 12 * 2 * Math.PI;\n  const offset = circumference * (1 - percent);\n\n  return (\n    <svg viewBox=\"0 0 24 24\">\n      <circle stroke=\"white\" strokeWidth=\"2\" fill=\"transparent\" cx=\"12\" cy=\"12\" r=\"12\"/>\n      <style jsx>{`\n          svg {\n            width: 100%;\n            height: 100%;\n            overflow: visible;\n            transform: rotate(-90deg);\n          }\n\n          circle {\n            stroke-dasharray: ${circumference} ${circumference};\n            stroke-dashoffset: ${offset};\n            ${percent === 0 ? '' : 'transition: stroke-dashoffset 0.35s;'}\n          }\n        `}</style>\n    </svg>\n  );\n};\n\nexport default VideoPreview;\n"
  },
  {
    "path": "renderer/components/editor/editor-preview.tsx",
    "content": "import TrafficLights from '../traffic-lights';\nimport VideoPlayer from './video-player';\nimport Options from './options';\nimport useEditorWindowState from 'hooks/editor/use-editor-window-state';\n\nconst EditorPreview = () => {\n  const {title = 'Editor'} = useEditorWindowState();\n\n  return (\n    <div className=\"preview-container\">\n      <div className=\"preview-hover-container\">\n        <div className=\"title-bar\">\n          <div className=\"title-bar-container\">\n            <TrafficLights/>\n            <div className=\"title\">{title}</div>\n          </div>\n        </div>\n        <VideoPlayer/>\n      </div>\n      <Options/>\n      <style jsx>{`\n        .preview-container {\n          display: flex;\n          flex-direction: column;\n          flex: 1;\n        }\n\n        .preview-hover-container {\n          display: flex;\n          flex: 1;\n          flex-direction: column;\n        }\n\n        .title-bar {\n          position: absolute;\n          top: -36px;\n          left: 0;\n          width: 100%;\n          height: 36px;\n          background: rgba(0, 0, 0, 0.2);\n          backdrop-filter: blur(20px);\n          transition: top 0.12s ease-in-out;\n          display: flex;\n          z-index: 10;\n        }\n\n        .preview-hover-container:hover .title-bar {\n          top: 0;\n        }\n\n        .title-bar-container {\n          flex: 1;\n          height: 100%;\n          display: flex;\n          align-items: center;\n        }\n\n        .title {\n          width: 100%;\n          height: 100%;\n          display: flex;\n          align-items: center;\n          justify-content: center;\n          font-size: 1.4rem;\n          color: #fff;\n          margin-left: -72px;\n        }\n      `}</style>\n    </div>\n  );\n};\n\nexport default EditorPreview;\n"
  },
  {
    "path": "renderer/components/editor/index.tsx",
    "content": "import useConversionIdContext from 'hooks/editor/use-conversion-id';\nimport useEditorWindowState from 'hooks/editor/use-editor-window-state';\nimport {useEditorWindowSizeEffect} from 'hooks/editor/use-window-size';\nimport {useEffect, useState} from 'react';\nimport EditorConversionView from './conversion';\nimport EditorPreview from './editor-preview';\nimport classNames from 'classnames';\n\nconst Editor = () => {\n  const {conversionId, setConversionId} = useConversionIdContext();\n  const state = useEditorWindowState();\n  const [isConversionPreviewState, setIsConversionPreviewState] = useState(false);\n\n  useEffect(() => {\n    if (state.conversionId && !conversionId) {\n      setConversionId(state.conversionId);\n    }\n  }, [state.conversionId]);\n\n  useEditorWindowSizeEffect(isConversionPreviewState);\n\n  const isTransitioning = Boolean(conversionId) !== isConversionPreviewState;\n\n  const className = classNames('container', {\n    transitioning: isTransitioning\n  });\n\n  const onTransitionEnd = () => {\n    setIsConversionPreviewState(Boolean(conversionId));\n  };\n\n  return (\n    <div\n      className={className}\n      onTransitionEnd={onTransitionEnd}\n    >\n      {\n        isConversionPreviewState ?\n          <EditorConversionView conversionId={conversionId}/> :\n          <EditorPreview/>\n      }\n      <style jsx>{`\n        .container {\n          flex: 1;\n          display: flex;\n          transition: opacity 1.4s ease-out;\n          opacity: 1;\n        }\n\n        .transitioning {\n          opacity: 0;\n          transition: opacity 0.4s ease-in-out;\n        }\n      `}</style>\n    </div>\n  );\n};\n\nexport default Editor;\n"
  },
  {
    "path": "renderer/components/editor/options/index.tsx",
    "content": "import LeftOptions from './left';\nimport RightOptions from './right';\n\nconst Options = () => {\n  return (\n    <div className=\"container\">\n      <LeftOptions/>\n      <RightOptions/>\n      <style jsx>{`\n          .container {\n            display: flex;\n            flex: 1;\n            padding: 0 16px;\n            align-items: center;\n            justify-content: space-between;\n            width: 100%;\n            background: var(--background-color);\n            z-index: 99;\n            height: 48px;\n            max-height: 48px;\n            flex-shrink: 0;\n          }\n        `}</style>\n    </div>\n  );\n};\n\nexport default Options;\n"
  },
  {
    "path": "renderer/components/editor/options/left.tsx",
    "content": "import css from 'styled-jsx/css';\nimport KeyboardNumberInput from '../../keyboard-number-input';\nimport Slider from './slider';\nimport OptionsContainer from '../options-container';\nimport {useState, useEffect, useMemo} from 'react';\nimport * as stringMath from 'string-math';\nimport VideoMetadataContainer from '../video-metadata-container';\nimport {shake} from '../../../utils/inputs';\nimport Select, {Separator} from './select';\n\nconst percentValues = [100, 75, 50, 33, 25, 20, 10];\n\nconst {className: keyboardInputClass, styles: keyboardInputStyles} = css.resolve`\n  height: 24px;\n  background: rgba(255, 255, 255, 0.1);\n  text-align: center;\n  font-size: 12px;\n  box-sizing: border-box;\n  border: none;\n  padding: 4px;\n  border-bottom-left-radius: 4px;\n  border-top-left-radius: 4px;\n  width: 48px;\n  color: white;\n  box-shadow: inset 0px 1px 0px 0px rgba(255, 255, 255, 0.04), 0px 1px 2px 0px rgba(0, 0, 0, 0.2);\n\n  input + input {\n    border-bottom-left-radius: 0;\n    border-top-left-radius: 0;\n    border-bottom-right-radius: 4px;\n    border-top-right-radius: 4px;\n    margin-left: 1px;\n    margin-right: 16px;\n  }\n\n  :focus, :hover {\n    outline: none;\n    background: hsla(0, 0%, 100%, 0.2);\n  }\n`;\n\nconst LeftOptions = () => {\n  const {width, height, setDimensions, fps, updateFps, originalFps} = OptionsContainer.useContainer();\n  const metadata = VideoMetadataContainer.useContainer();\n\n  const [widthValue, setWidthValue] = useState<string>();\n  const [heightValue, setHeightValue] = useState<string>();\n\n  const onChange = (event, {ignoreEmpty = true}: {ignoreEmpty?: boolean} = {}) => {\n    if (!ignoreEmpty) {\n      onBlur(event);\n      return;\n    }\n\n    const {currentTarget: {name, value}} = event;\n    if (name === 'width') {\n      setWidthValue(value);\n    } else {\n      setHeightValue(value);\n    }\n  };\n\n  const onBlur = event => {\n    const {currentTarget} = event;\n    const {name} = currentTarget;\n\n    let value: number;\n    try {\n      value = stringMath(currentTarget.value);\n    } catch {}\n\n    // Fallback to last valid\n    const updates = {width, height};\n\n    if (value) {\n      value = Math.round(value);\n      const ratio = metadata.width / metadata.height;\n\n      if (name === 'width') {\n        const min = Math.max(1, Math.ceil(ratio));\n\n        if (value < min) {\n          shake(currentTarget, {className: 'shake-left'});\n          updates.width = min;\n        } else if (value > metadata.width) {\n          shake(currentTarget, {className: 'shake-left'});\n          updates.width = metadata.width;\n        } else {\n          updates.width = value;\n        }\n\n        updates.height = Math[ratio > 1 ? 'ceil' : 'floor'](updates.width / ratio);\n      } else {\n        const min = Math.max(1, Math.ceil(1 / ratio));\n\n        if (value < min) {\n          shake(currentTarget, {className: 'shake-right'});\n          updates.height = min;\n        } else if (value > metadata.height) {\n          shake(currentTarget, {className: 'shake-right'});\n          updates.height = metadata.height;\n        } else {\n          updates.height = value;\n        }\n\n        updates.width = Math[ratio > 1 ? 'floor' : 'ceil'](updates.height * ratio);\n      }\n    } else if (name === 'width') {\n      shake(currentTarget, {className: 'shake-left'});\n    } else {\n      shake(currentTarget, {className: 'shake-right'});\n    }\n\n    setDimensions(updates);\n    setWidthValue(updates.width.toString());\n    setHeightValue(updates.height.toString());\n  };\n\n  useEffect(() => {\n    if (width && height) {\n      setWidthValue(width.toString());\n      setHeightValue(height.toString());\n    }\n  }, [width, height]);\n\n  const percentOptions = useMemo(() => {\n    const ratio = metadata.width / metadata.height;\n\n    const options = percentValues.map(percent => {\n      const adjustedWidth = Math.round(metadata.width * (percent / 100));\n      const adjustedHeight = Math[ratio > 1 ? 'ceil' : 'floor'](adjustedWidth / ratio);\n\n      return {\n        label: `${adjustedWidth} x ${adjustedHeight} (${percent === 100 ? 'Original' : `${percent}%`})`,\n        value: {width: adjustedWidth, height: adjustedHeight},\n        checked: width === adjustedWidth\n      };\n    });\n\n    if (options.every(opt => !opt.checked)) {\n      return [\n        {\n          label: 'Custom',\n          value: {width, height},\n          checked: true\n        },\n        {\n          separator: true\n        },\n        ...options\n      ];\n    }\n\n    return options;\n  }, [metadata, width, height]);\n\n  const selectPercentage = updates => {\n    setDimensions(updates);\n    setWidthValue(updates.width.toString());\n    setHeightValue(updates.height.toString());\n  };\n\n  const percentLabel = `${Math.round((width / metadata.width) * 100)}%`;\n\n  return (\n    <div className=\"container\">\n      <div className=\"label\">Size</div>\n      <KeyboardNumberInput\n        className={keyboardInputClass}\n        value={widthValue || ''}\n        size=\"5\"\n        name=\"width\"\n        min={1}\n        max={metadata.width}\n        onChange={onChange}\n        onBlur={onBlur}\n      />\n      <KeyboardNumberInput\n        className={keyboardInputClass}\n        value={heightValue || ''}\n        size=\"5\"\n        name=\"height\"\n        min={1}\n        max={metadata.height}\n        onChange={onChange}\n        onBlur={onBlur}\n      />\n      <div className=\"percent\">\n        <Select options={percentOptions as any} customLabel={percentLabel} onChange={selectPercentage}/>\n      </div>\n      <div className=\"label\">FPS</div>\n      <div className=\"fps\">\n        <Slider value={fps} min={5} max={originalFps} onChange={updateFps}/>\n      </div>\n      {keyboardInputStyles}\n      <style jsx>{`\n          .container {\n            height: 100%;\n            display: flex;\n            align-items: center;\n          }\n\n          .label {\n            font-size: 12px;\n            margin-right: 8px;\n            color: white;\n          }\n\n          .percent {\n            height: 24px;\n            width: 68px;\n            margin-left: -8px;\n            margin-right: 8px;\n          }\n\n          .fps {\n            height: 24px;\n            width: 32px;\n          }\n\n          .option {\n            width: 48px;\n            height: 22px;\n            background: transparent;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n            font-size: 12px;\n            color: white;\n            box-sizing: border-box;\n          }\n\n          .option:hover {\n            background: hsla(0, 0%, 100%, 0.2);\n          }\n\n          .option:active,\n          .option.selected {\n            background: transparent;\n          }\n        `}</style>\n    </div>\n  );\n};\n\nexport default LeftOptions;\n\n// Import React from 'react';\n// import PropTypes from 'prop-types';\n// import {connect, EditorContainer} from '../../../containers';\n// import css from 'styled-jsx/css';\n\n// import KeyboardNumberInput from '../../keyboard-number-input';\n// import Slider from './slider';\n\n// const {className: keyboardInputClass, styles: keyboardInputStyles} = css.resolve`\n//   height: 24px;\n//   background: rgba(255, 255, 255, 0.1);\n//   text-align: center;\n//   font-size: 12px;\n//   box-sizing: border-box;\n//   border: none;\n//   padding: 4px;\n//   border-bottom-left-radius: 4px;\n//   border-top-left-radius: 4px;\n//   width: 48px;\n//   color: white;\n//   box-shadow: inset 0px 1px 0px 0px rgba(255, 255, 255, 0.04), 0px 1px 2px 0px rgba(0, 0, 0, 0.2);\n\n//   input + input {\n//     border-bottom-left-radius: 0;\n//     border-top-left-radius: 0;\n//     border-bottom-right-radius: 4px;\n//     border-top-right-radius: 4px;\n//     margin-left: 1px;\n//     margin-right: 16px;\n//   }\n\n//   :focus, :hover {\n//     outline: none;\n//     background: hsla(0, 0%, 100%, 0.2);\n//   }\n// `;\n\n// class LeftOptions extends React.Component {\n//   handleBlur = event => {\n//     const {changeDimension} = this.props;\n//     changeDimension(event, {ignoreEmpty: false});\n//   }\n\n//   render() {\n// const {width, height, changeDimension, fps, originalFps, setFps, original} = this.props;\n\n// return (\n//   <div className=\"container\">\n//     <div className=\"label\">Size</div>\n//     <KeyboardNumberInput\n//       className={keyboardInputClass}\n//       value={width || ''}\n//       size=\"5\"\n//       min={1}\n//       max={original && original.width}\n//       name=\"width\"\n//       onChange={changeDimension}\n//       onKeyDown={changeDimension}\n//       onBlur={this.handleBlur}\n//     />\n//     <KeyboardNumberInput\n//       className={keyboardInputClass}\n//       value={height || ''}\n//       size=\"5\"\n//       min={1}\n//       max={original && original.height}\n//       name=\"height\"\n//       onChange={changeDimension}\n//       onKeyDown={changeDimension}\n//       onBlur={this.handleBlur}\n//     />\n//     <div className=\"label\">FPS</div>\n//     <div className=\"fps\">\n//       <Slider value={fps} min={1} max={originalFps} onChange={setFps}/>\n//     </div>\n//     {keyboardInputStyles}\n//     <style jsx>{`\n//       .container {\n//         height: 100%;\n//         display: flex;\n//         align-items: center;\n//       }\n\n//       .label {\n//         font-size: 12px;\n//         margin-right: 8px;\n//         color: white;\n//       }\n\n//       .fps {\n//         height: 24px;\n//         width: 32px;\n//       }\n\n//       .option {\n//         width: 48px;\n//         height: 22px;\n//         background: transparent;\n//         display: flex;\n//         align-items: center;\n//         justify-content: center;\n//         font-size: 12px;\n//         color: white;\n//         box-sizing: border-box;\n//       }\n\n//       .option:hover {\n//         background: hsla(0, 0%, 100%, 0.2);\n//       }\n\n//       .option:active,\n//       .option.selected {\n//         background: transparent;\n//       }\n//     `}</style>\n//   </div>\n// );\n//   }\n// }\n\n// LeftOptions.propTypes = {\n//   width: PropTypes.number,\n//   height: PropTypes.number,\n//   changeDimension: PropTypes.elementType,\n//   fps: PropTypes.number,\n//   setFps: PropTypes.elementType,\n//   originalFps: PropTypes.number,\n//   original: PropTypes.shape({\n//     width: PropTypes.number,\n//     height: PropTypes.number\n//   })\n// };\n\n// export default connect(\n//   [EditorContainer],\n//   ({width, height, fps, originalFps, original}) => ({width, height, fps, originalFps, original}),\n//   ({changeDimension, setFps}) => ({changeDimension, setFps})\n// )(LeftOptions);\n"
  },
  {
    "path": "renderer/components/editor/options/right.tsx",
    "content": "import {GearIcon} from '../../../vectors';\nimport OptionsContainer from '../options-container';\nimport Select from './select';\nimport {ipcRenderer as ipc} from 'electron-better-ipc';\nimport useConversionIdContext from 'hooks/editor/use-conversion-id';\nimport useEditorWindowState from 'hooks/editor/use-editor-window-state';\nimport VideoTimeContainer from '../video-time-container';\nimport VideoControlsContainer from '../video-controls-container';\nimport useSharePlugins from 'hooks/editor/use-share-plugins';\nimport useEditorOptions from 'hooks/editor/use-editor-options';\n\nconst FormatSelect = () => {\n  const {formats, format, updateFormat} = OptionsContainer.useContainer();\n  const options = formats.map(format => ({label: format.prettyFormat, value: format.format}));\n\n  return <Select options={options} value={format} onChange={updateFormat}/>;\n};\n\nconst PluginsSelect = () => {\n  const {menuOptions, label, onChange} = useSharePlugins();\n  return <Select options={menuOptions} customLabel={label} onChange={onChange}/>;\n};\n\nconst EditPluginsControl = () => {\n  const {editServices, editPlugin, setEditPlugin} = OptionsContainer.useContainer();\n\n  if (editServices?.length === 0) {\n    return null;\n  }\n\n  if (!editPlugin) {\n    return (\n      <button\n        type=\"button\" className=\"add-edit-plugin\" onClick={() => {\n          setEditPlugin(editServices[0]);\n        }}\n      >\n        +\n        <style jsx>{`\n          button {\n            padding: 4px 8px;\n            background: rgba(255, 255, 255, 0.1);\n            font-size: 12px;\n            line-height: 12px;\n            color: white;\n            height: 24px;\n            border-radius: 4px;\n            text-align: center;\n            border: none;\n            box-shadow: inset 0px 1px 0px 0px rgba(255, 255, 255, 0.04), 0px 1px 2px 0px rgba(0, 0, 0, 0.2);\n          }\n\n          button:hover,\n          button:focus {\n            background: hsla(0, 0%, 100%, 0.2);\n            outline: none;\n          }\n\n          .add-edit-plugin {\n            width: max-content;\n            margin-right: 8px;\n          }\n        `}</style>\n      </button>\n    );\n  }\n\n  const openEditPluginConfig = () => {\n    ipc.callMain('open-edit-config', {\n      pluginName: editPlugin.pluginName,\n      serviceTitle: editPlugin.title\n    });\n  };\n\n  const options = editServices.map(service => ({label: service.title, value: service}));\n\n  return (\n    <>\n      {\n        editPlugin.hasConfig && (\n          <button type=\"button\" className=\"add-edit-plugin\" onClick={openEditPluginConfig}>\n            <GearIcon fill=\"#fff\" hoverFill=\"#fff\" size=\"12px\"/>\n          </button>\n        )\n      }\n      <div className=\"edit-plugin\">\n        <Select clearable options={options} value={editPlugin} onChange={setEditPlugin}/>\n      </div>\n      <style jsx>{`\n        button {\n          padding: 4px 8px;\n          background: rgba(255, 255, 255, 0.1);\n          font-size: 12px;\n          line-height: 12px;\n          color: white;\n          height: 24px;\n          border-radius: 4px;\n          text-align: center;\n          border: none;\n          box-shadow: inset 0px 1px 0px 0px rgba(255, 255, 255, 0.04), 0px 1px 2px 0px rgba(0, 0, 0, 0.2);\n        }\n\n        button:hover,\n        button:focus {\n          background: hsla(0, 0%, 100%, 0.2);\n          outline: none;\n        }\n\n        .add-edit-plugin {\n          width: max-content;\n          margin-right: 8px;\n        }\n\n        .edit-plugin {\n          height: 24px;\n          margin-right: 8px;\n          width: 128px;\n        }\n      `}</style>\n    </>\n  );\n};\n\nconst ConvertButton = () => {\n  const {startConversion} = useConversionIdContext();\n  const options = OptionsContainer.useContainer();\n  const {filePath} = useEditorWindowState();\n  const {startTime, endTime} = VideoTimeContainer.useContainer();\n  const {isMuted} = VideoControlsContainer.useContainer();\n  const {updatePluginUsage} = useEditorOptions();\n\n  const onClick = () => {\n    const shouldCrop = true;\n    startConversion({\n      filePath,\n      conversionOptions: {\n        width: options.width,\n        height: options.height,\n        startTime,\n        endTime,\n        fps: options.fps,\n        shouldMute: isMuted,\n        shouldCrop,\n        editService: options.editPlugin ? {\n          pluginName: options.editPlugin.pluginName,\n          serviceTitle: options.editPlugin.title\n        } : undefined\n      },\n      format: options.format,\n      plugins: {\n        share: options.sharePlugin\n      }\n    });\n\n    updatePluginUsage({\n      format: options.format,\n      plugin: options.sharePlugin.pluginName\n    });\n  };\n\n  return (\n    <button type=\"button\" className=\"start-export\" onClick={onClick}>\n      Convert\n      <style jsx>{`\n        button {\n          padding: 4px 8px;\n          background: rgba(255, 255, 255, 0.1);\n          font-size: 12px;\n          line-height: 12px;\n          color: white;\n          height: 24px;\n          border-radius: 4px;\n          text-align: center;\n          border: none;\n          box-shadow: inset 0px 1px 0px 0px rgba(255, 255, 255, 0.04), 0px 1px 2px 0px rgba(0, 0, 0, 0.2);\n        }\n\n        button:hover,\n        button:focus {\n          background: hsla(0, 0%, 100%, 0.2);\n          outline: none;\n        }\n\n        .start-export {\n          width: 72px;\n        }\n      `}</style>\n    </button>\n  );\n};\n\nconst RightOptions = () => {\n  return (\n    <div className=\"container\">\n      <EditPluginsControl/>\n      <div className=\"format\"><FormatSelect/></div>\n      <div className=\"plugin\"><PluginsSelect/></div>\n      <ConvertButton/>\n      <style jsx>{`\n          .container {\n            height: 100%;\n            display: flex;\n            align-items: center;\n          }\n\n          .label {\n            font-size: 12px;\n            margin-right: 8px;\n            color: white;\n          }\n\n          .format {\n            height: 24px;\n            width: 112px;\n            margin-right: 8px;\n          }\n\n          .plugin {\n            height: 24px;\n            width: 128px;\n            margin-right: 8px;\n          }\n        `}</style>\n    </div>\n  );\n};\n\nexport default RightOptions;\n\n// Import electron from 'electron';\n// import React from 'react';\n// import PropTypes from 'prop-types';\n\n// import {connect, EditorContainer} from '../../../containers';\n// import Select from './select';\n// import {GearIcon} from '../../../vectors';\n\n// class RightOptions extends React.Component {\n//   render() {\n//     const {\n//       options,\n//       format,\n//       plugin,\n//       selectFormat,\n//       selectPlugin,\n//       startExport,\n//       openWithApp,\n//       selectOpenWithApp,\n//       selectEditPlugin,\n//       editOptions,\n//       editPlugin,\n//       openEditPluginConfig\n//     } = this.props;\n\n//     const formatOptions = options ? options.map(({format, prettyFormat}) => ({value: format, label: prettyFormat})) : [];\n//     const pluginOptions = options ? options.find(option => option.format === format).plugins.map(plugin => {\n//       if (plugin.apps) {\n//         const submenu = plugin.apps.map(app => ({\n//           label: app.isDefault ? `${app.name} (default)` : app.name,\n//           type: 'radio',\n//           checked: openWithApp && app.url === openWithApp.url,\n//           click: () => selectOpenWithApp(app),\n//           icon: electron.remote.nativeImage.createFromDataURL(app.icon).resize({width: 16, height: 16})\n//         }));\n\n//         if (plugin.apps[0].isDefault) {\n//           submenu.splice(1, 0, {type: 'separator'});\n//         }\n\n//         return {\n//           isBuiltIn: false,\n//           submenu,\n//           value: plugin.title,\n//           label: openWithApp ? openWithApp.name : ''\n//         };\n//       }\n\n//       return {\n//         type: openWithApp ? 'normal' : 'radio',\n//         value: plugin.title,\n//         label: plugin.title,\n//         isBuiltIn: plugin.pluginName.startsWith('_')\n//       };\n//     }) : [];\n\n//     if (pluginOptions.every(opt => opt.isBuiltIn)) {\n//       pluginOptions.push({\n//         separator: true\n//       }, {\n//         type: 'normal',\n//         label: 'Get Plugins…',\n//         value: 'open-plugins'\n//       });\n//     }\n\n//     const editPluginOptions = editOptions && editOptions.map(option => ({label: option.title, value: option}));\n//     const buttonAction = editPlugin ? openEditPluginConfig : () => selectEditPlugin(editOptions[0]);\n\n// return (\n//   <div className=\"container\">\n//     {\n//       editPluginOptions && editPluginOptions.length > 0 && (\n//         <>\n//           {\n//             (!editPlugin || editPlugin.hasConfig) && (\n//               <button key={editPlugin} type=\"button\" className=\"add-edit-plugin\" onClick={buttonAction}>\n//                 {editPlugin ? <GearIcon fill=\"#fff\" hoverFill=\"#fff\" size=\"12px\"/> : '+'}\n//               </button>\n//             )\n//           }\n//           {\n//             editPlugin && (\n//               <div className=\"edit-plugin\">\n//                 <Select clearable options={editPluginOptions} selected={editPlugin} onChange={selectEditPlugin}/>\n//               </div>\n//             )\n//           }\n//         </>\n//       )\n//     }\n//     <div className=\"format\">\n//       <Select options={formatOptions} selected={format} onChange={selectFormat}/>\n//     </div>\n//     <div className=\"plugin\">\n//       <Select options={pluginOptions} selected={plugin} onChange={selectPlugin}/>\n//     </div>\n//     <button type=\"button\" className=\"start-export\" onClick={startExport}>Export</button>\n//     <style jsx>{`\n//       .container {\n//         height: 100%;\n//         display: flex;\n//         align-items: center;\n//       }\n\n//       .label {\n//         font-size: 12px;\n//         margin-right: 8px;\n//         color: white;\n//       }\n\n//       .format {\n//         height: 24px;\n//         width: 96px;\n//         margin-right: 8px;\n//       }\n\n//       .edit-plugin {\n//         height: 24px;\n//         margin-right: 8px;\n//         width: 128px;\n//       }\n\n// .plugin {\n//   height: 24px;\n//   width: 128px;\n//   margin-right: 8px;\n// }\n\n//       button {\n//         padding: 4px 8px;\n//         background: rgba(255, 255, 255, 0.1);\n//         font-size: 12px;\n//         line-height: 12px;\n//         color: white;\n//         height: 24px;\n//         border-radius: 4px;\n//         text-align: center;\n//         border: none;\n//         box-shadow: inset 0px 1px 0px 0px rgba(255, 255, 255, 0.04), 0px 1px 2px 0px rgba(0, 0, 0, 0.2);\n//       }\n\n//       button:hover,\n//       button:focus {\n//         background: hsla(0, 0%, 100%, 0.2);\n//         outline: none;\n//       }\n\n//       .start-export {\n//         width: 72px;\n//       }\n\n//       .add-edit-plugin {\n//         width: max-content;\n//         margin-right: 8px;\n//       }\n//     `}</style>\n//   </div>\n// );\n//   }\n// }\n\n// RightOptions.propTypes = {\n//   options: PropTypes.arrayOf(PropTypes.object),\n//   format: PropTypes.string,\n//   plugin: PropTypes.string,\n//   selectFormat: PropTypes.elementType,\n//   selectPlugin: PropTypes.elementType,\n//   startExport: PropTypes.elementType,\n//   openWithApp: PropTypes.object,\n//   selectOpenWithApp: PropTypes.elementType,\n//   editPlugin: PropTypes.object,\n//   editOptions: PropTypes.arrayOf(PropTypes.object),\n//   selectEditPlugin: PropTypes.elementType,\n//   openEditPluginConfig: PropTypes.elementType\n// };\n\n// export default connect(\n//   [EditorContainer],\n//   ({options, format, plugin, openWithApp, editOptions, editPlugin}) => ({options, format, plugin, openWithApp, editOptions, editPlugin}),\n//   ({selectFormat, selectPlugin, startExport, selectOpenWithApp, selectEditPlugin, openEditPluginConfig}) => ({selectFormat, selectPlugin, startExport, selectOpenWithApp, selectEditPlugin, openEditPluginConfig})\n// )(RightOptions);\n"
  },
  {
    "path": "renderer/components/editor/options/select.tsx",
    "content": "import {DropdownArrowIcon, CancelIcon} from '../../../vectors';\nimport classNames from 'classnames';\nimport {useRef} from 'react';\nimport {remote, MenuItemConstructorOptions, NativeImage} from 'electron';\n\ntype Option<T> = {\n  label: string;\n  value: T;\n  subMenu?: Array<Option<T>>;\n  type?: string;\n  checked?: boolean;\n  click?: () => void;\n  separator?: false;\n  icon?: NativeImage;\n};\n\nexport type Separator = {\n  value: never;\n  label: never;\n  subMenu: never;\n  type: never;\n  checked: never;\n  icon: never;\n  separator: true;\n};\n\ninterface Props<T> {\n  value?: T;\n  options: Array<Option<T> | Separator>;\n  onChange: (newValue?: T) => void;\n  clearable?: boolean;\n  customLabel?: string;\n}\n\n// eslint-disable-next-line @typescript-eslint/comma-dangle\nconst Select = <T, >(props: Props<T>) => {\n  const select = useRef<HTMLDivElement>();\n  const {options = [], value} = props;\n\n  const selectedOption = options.find(opt => opt.value === value);\n  const selectedLabel = props.customLabel ?? (selectedOption?.label);\n  const clearable = props.clearable && selectedOption;\n\n  const handleClick = () => {\n    if (options.length === 0) {\n      return;\n    }\n\n    const boundingRect = select.current.getBoundingClientRect();\n\n    const {Menu} = remote;\n\n    const convertToMenuTemplate = (option: Option<T> | Separator): MenuItemConstructorOptions => {\n      if (option.separator) {\n        return {type: 'separator'};\n      }\n\n      if (option.subMenu) {\n        return {\n          label: option.label,\n          submenu: option.subMenu.map(opt => convertToMenuTemplate(opt)),\n          checked: option.checked\n        };\n      }\n\n      return {\n        label: option.label,\n        type: option.type as any || 'checkbox',\n        checked: option.checked ?? (option.value === value),\n        click: option.click ?? (() => {\n          props.onChange(option.value);\n        }),\n        icon: option.icon\n      };\n    };\n\n    const menu = Menu.buildFromTemplate(options.map(opt => convertToMenuTemplate(opt)));\n\n    menu.popup({\n      x: Math.round(boundingRect.left),\n      y: Math.round(boundingRect.top)\n    });\n  };\n\n  const handleDropdownClick = event => {\n    if (clearable) {\n      event.stopPropagation();\n      props.onChange();\n    }\n  };\n\n  return (\n    <div ref={select} className=\"container\" onClick={handleClick}>\n      <div className=\"label\">{selectedLabel}</div>\n      <div className={classNames({dropdown: true, clearable})} onClick={handleDropdownClick}>\n        {\n          clearable ?\n            <CancelIcon size=\"16px\"/> :\n            <DropdownArrowIcon/>\n        }\n      </div>\n      <style jsx>{`\n        .container {\n          width: 100%;\n          height: 100%;\n          position: relative;\n          border-radius: 4px;\n          padding: 4px 8px;\n          font-size: 12px;\n          color: white;\n          display: flex;\n          justify-content: space-between;\n          box-shadow: inset 0px 1px 0px 0px rgba(255, 255, 255, 0.04), 0px 1px 2px 0px rgba(0, 0, 0, 0.2);\n          background: rgba(255, 255, 255, 0.1);\n        }\n\n        .label {\n          flex: 1;\n          white-space: nowrap;\n          overflow: hidden;\n          text-overflow: ellipsis;\n        }\n\n        .container:hover,\n        .container:focus {\n          background: hsla(0, 0%, 100%, 0.2);\n        }\n\n        .dropdown {\n          display: flex;\n          justify-content: center;\n          align-items: center;\n          width: 18px;\n          pointer-events: none;\n        }\n\n        .clearable {\n          pointer-events: auto;\n        }\n      `}</style>\n    </div>\n  );\n};\n\nexport default Select;\n"
  },
  {
    "path": "renderer/components/editor/options/slider.tsx",
    "content": "import {TooltipIcon} from '../../../vectors';\nimport {useState, useEffect} from 'react';\nimport {shake} from '../../../utils/inputs';\n\ninterface Props {\n  value: number;\n  onChange: (newValue: number) => void;\n  min: number;\n  max: number;\n}\n\nconst Slider = (props: Props) => {\n  const [isOpen, setIsOpen] = useState(false);\n  const [valueText, setValueText] = useState(props.value?.toString());\n\n  useEffect(() => {\n    setValueText(props.value?.toString());\n  }, [props.value]);\n\n  const onChange = event => {\n    setValueText(event.currentTarget.value);\n  };\n\n  const onBlur = event => {\n    const {currentTarget} = event;\n    const value = Number.parseInt(currentTarget.value, 10);\n\n    if (value && value >= props.min && value <= props.max) {\n      props.onChange(value);\n      setValueText(value.toString());\n    } else if (value) {\n      const newValue = Math.min(Math.max(value, props.min), props.max);\n      props.onChange(newValue);\n      setValueText(newValue.toString());\n      shake(currentTarget);\n    } else {\n      setValueText(props.value.toString());\n      shake(currentTarget);\n    }\n  };\n\n  const onKeyDown = event => {\n    if (event.key === 'Enter') {\n      onBlur(event);\n    }\n  };\n\n  const onSliderChange = event => {\n    const value = Number.parseInt(event.currentTarget.value, 10);\n    props.onChange(value);\n    setValueText(value.toString());\n  };\n\n  return (\n    <div className=\"container\">\n      {isOpen && <div\n        className=\"overlay\" onClick={() => {\n          setIsOpen(false);\n        }}/>}\n      <input\n        type=\"text\"\n        className=\"value\"\n        value={valueText || ''}\n        onChange={onChange}\n        onKeyDown={onKeyDown}\n        onBlur={onBlur}\n        onFocus={() => {\n          setIsOpen(true);\n        }}\n      />\n      {\n        isOpen && (\n          <div\n            className=\"popup\" onClick={event => {\n              event.stopPropagation();\n            }}\n          >\n            <input\n              type=\"range\"\n              className=\"slider\"\n              min={props.min}\n              max={props.max}\n              step={1}\n              value={props.value || props.min}\n              onChange={onSliderChange}\n              onBlur={() => {\n                setIsOpen(false);\n              }}\n            />\n            <div className=\"arrow\">\n              <TooltipIcon fill=\"var(--slider-popup-background)\" hoverFill=\"var(--slider-popup-background)\"/>\n            </div>\n          </div>\n        )\n      }\n      <style jsx>{`\n          .container {\n            width: 100%;\n            height: 100%;\n            position: relative;\n            font-size: 12px;\n            color: white;\n          }\n\n          .value {\n            width: 100%;\n            height: 100%;\n            background: rgba(255, 255, 255, 0.1);\n            border-radius: 4px;\n            padding: 4px 8px;\n            text-align: center;\n            font-size: 12px;\n            -webkit-appearance: none;\n            outline: none;\n            color: white;\n            border: none;\n            z-index: 50;\n            position: relative;\n            box-shadow: inset 0px 1px 0px 0px rgba(255, 255, 255, 0.04), 0px 1px 2px 0px rgba(0, 0, 0, 0.2);\n          }\n\n          .value:hover,\n          .value:focus {\n            background: hsla(0, 0%, 100%, 0.2);\n          }\n\n          .arrow {\n            position: absolute;\n            width: 24px;\n            height: 12px;\n            top: 100%;\n            left: 50%;\n            transform: translateX(-50%);\n          }\n\n          .popup {\n            position: absolute;\n            height: 48px;\n            padding: 0 32px;\n            bottom: 100%;\n            left: 50%;\n            transform: translateX(-50%);\n            margin-bottom: 16px;\n            background: var(--slider-popup-background);\n            box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.40);\n            z-index: 50;\n            border-radius: 2px;\n            -webkit-app-region: no-drag;\n            display: flex;\n            align-items: center;\n          }\n\n          .overlay {\n            position: fixed;\n            top: 0;\n            left: 0;\n            width: 100%;\n            height: 100%;\n            background: transparent;\n            z-index: 49;\n          }\n\n          .slider {\n            width: 144px;\n            -webkit-appearance: none;\n            outline: none;\n            background: transparent;\n            z-index: 20;\n          }\n\n          .slider::-webkit-slider-runnable-track {\n            width: 100%;\n            height: 4px;\n            border-color: transparent;\n            background: var(--slider-background-color);\n            border-radius: 4px;\n            box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.4);\n          }\n\n          .slider::-webkit-slider-thumb {\n            -webkit-appearance: none;\n            height: 16px;\n            width: 16px;\n            border-radius: 50%;\n            background: var(--slider-thumb-color);\n            box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.4);\n            margin-top: -6px;\n            z-index: 50;\n          }\n\n          .slider:focus::-webkit-slider-thumb {\n            border: 1px solid var(--kap);\n          }\n        `}</style>\n    </div>\n  );\n};\n\nexport default Slider;\n\n// Import React from 'react';\n// import PropTypes from 'prop-types';\n\n// class Slider extends React.Component {\n//   state = {\n//     isOpen: false\n//   }\n\n//   show = () => this.setState({isOpen: true})\n\n//   hide = () => this.setState({isOpen: false})\n\n//   handleChange = event => {\n//     const {onChange} = this.props;\n//     onChange(event.target.value, event.target);\n//   }\n\n//   handleBlur = event => {\n//     const {onChange} = this.props;\n//     onChange(event.target.value, event.target, {ignoreEmpty: false});\n//   }\n\n//   render() {\n//     const {value, max, min} = this.props;\n//     const {isOpen} = this.state;\n\n// return (\n//   <div className=\"container\">\n//     { isOpen && <div className=\"overlay\" onClick={this.hide}/> }\n//     <input type=\"text\" className=\"value\" value={value || ''} onChange={this.handleChange} onBlur={this.handleBlur} onFocus={this.show}/>\n//     {\n//       isOpen && (\n//         <div className=\"popup\" onClick={event => event.stopPropagation()}>\n//           <input type=\"range\" className=\"slider\" min={min} max={max} step={1} value={value || min} onChange={this.handleChange}/>\n//           <div className=\"arrow\">\n//             <TooltipIcon fill=\"var(--slider-popup-background)\" hoverFill=\"var(--slider-popup-background)\"/>\n//           </div>\n//         </div>\n//       )\n//     }\n//     <style jsx>{`\n//       .container {\n//         width: 100%;\n//         height: 100%;\n//         position: relative;\n//         font-size: 12px;\n//         color: white;\n//       }\n\n//       .value {\n//         width: 100%;\n//         height: 100%;\n//         background: rgba(255, 255, 255, 0.1);\n//         border-radius: 4px;\n//         padding: 4px 8px;\n//         text-align: center;\n//         font-size: 12px;\n//         -webkit-appearance: none;\n//         outline: none;\n//         color: white;\n//         border: none;\n//         z-index: 50;\n//         position: relative;\n//         box-shadow: inset 0px 1px 0px 0px rgba(255, 255, 255, 0.04), 0px 1px 2px 0px rgba(0, 0, 0, 0.2);\n//       }\n\n//       .value:hover,\n//       .value:focus {\n//         background: hsla(0, 0%, 100%, 0.2);\n//       }\n\n//       .arrow {\n//         position: absolute;\n//         width: 24px;\n//         height: 12px;\n//         top: 100%;\n//         left: 50%;\n//         transform: translateX(-50%);\n//       }\n\n//       .popup {\n//         position: absolute;\n//         height: 48px;\n//         padding: 0 32px;\n//         bottom: 100%;\n//         left: 50%;\n//         transform: translateX(-50%);\n//         margin-bottom: 16px;\n//         background: var(--slider-popup-background);\n//         box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.40);\n//         z-index: 50;\n//         border-radius: 2px;\n//         -webkit-app-region: no-drag;\n//         display: flex;\n//         align-items: center;\n//       }\n\n//       .overlay {\n//         position: fixed;\n//         top: 0;\n//         left: 0;\n//         width: 100%;\n//         height: 100%;\n//         background: transparent;\n//         z-index: 49;\n//       }\n\n//       .slider {\n//         width: 144px;\n//         -webkit-appearance: none;\n//         outline: none;\n//         background: transparent;\n//         z-index: 20;\n//       }\n\n//       .slider::-webkit-slider-runnable-track {\n//         width: 100%;\n//         height: 4px;\n//         border-color: transparent;\n//         background: var(--slider-background-color);\n//         border-radius: 4px;\n//         box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.4);\n//       }\n\n//       .slider::-webkit-slider-thumb {\n//         -webkit-appearance: none;\n//         height: 16px;\n//         width: 16px;\n//         border-radius: 50%;\n//         background: var(--slider-thumb-color);\n//         box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.4);\n//         margin-top: -6px;\n//         z-index: 50;\n//       }\n\n//       .slider:focus::-webkit-slider-thumb {\n//         border: 1px solid var(--kap);\n//       }\n//     `}</style>\n//   </div>\n// );\n//   }\n// }\n\n// Slider.propTypes = {\n//   value: PropTypes.number,\n//   max: PropTypes.number,\n//   min: PropTypes.number,\n//   onChange: PropTypes.elementType\n// };\n\n// export default Slider;\n"
  },
  {
    "path": "renderer/components/editor/options-container.tsx",
    "content": "import {useState, useEffect, useMemo} from 'react';\nimport {createContainer} from 'unstated-next';\nimport {debounce, DebouncedFunc} from 'lodash';\n\nimport VideoMetadataContainer from './video-metadata-container';\nimport VideoControlsContainer from './video-controls-container';\nimport useEditorOptions, {EditorOptionsState} from 'hooks/editor/use-editor-options';\nimport {Format, App} from 'common/types';\nimport useEditorWindowState from 'hooks/editor/use-editor-window-state';\n\ntype EditService = EditorOptionsState['editServices'][0];\n\ntype SharePlugin = {\n  pluginName: string;\n  serviceTitle: string;\n  app?: App;\n};\n\nconst isFormatMuted = (format: Format) => ['gif', 'apng'].includes(format);\n\nconst useOptions = () => {\n  const {fps: originalFps} = useEditorWindowState();\n  const {\n    state: {\n      formats,\n      fpsHistory,\n      editServices\n    },\n    updateFpsUsage,\n    isLoading\n  } = useEditorOptions();\n\n  const metadata = VideoMetadataContainer.useContainer();\n  const {isMuted, mute, unmute} = VideoControlsContainer.useContainer();\n\n  const [format, setFormat] = useState<Format>();\n  const [fps, setFps] = useState<number>();\n  const [width, setWidth] = useState<number>();\n  const [height, setHeight] = useState<number>();\n  const [editPlugin, setEditPlugin] = useState<EditService>();\n  const [sharePlugin, setSharePlugin] = useState<SharePlugin>();\n\n  const [wasMuted, setWasMuted] = useState(false);\n\n  const debouncedUpdateFpsUsage = useMemo(() => {\n    if (!updateFpsUsage) {\n      return;\n    }\n\n    return debounce(updateFpsUsage, 1000);\n  }, [updateFpsUsage]);\n\n  const updateFps = (newFps: number, formatName = format) => {\n    setFps(newFps);\n    debouncedUpdateFpsUsage?.({format: formatName, fps: newFps});\n  };\n\n  const updateSharePlugin = (plugin: SharePlugin) => {\n    setSharePlugin(plugin);\n  };\n\n  const updateFormat = (formatName: Format) => {\n    debouncedUpdateFpsUsage.flush();\n\n    if (metadata.hasAudio) {\n      if (isFormatMuted(formatName) && !isFormatMuted(format)) {\n        setWasMuted(isMuted);\n        mute();\n      } else if (!isFormatMuted(formatName) && isFormatMuted(format) && !wasMuted) {\n        unmute();\n      }\n    }\n\n    const formatOption = formats.find(f => f.format === formatName);\n    const selectedSharePlugin = formatOption.plugins.find(plugin => {\n      return (\n        plugin.pluginName === sharePlugin.pluginName &&\n        plugin.title === sharePlugin.serviceTitle &&\n        (plugin.apps?.some(app => app.url === sharePlugin.app?.url) ?? true)\n      );\n    }) ?? formatOption.plugins.find(plugin => plugin.pluginName !== '_openWith');\n\n    setFormat(formatName);\n    setSharePlugin({\n      pluginName: selectedSharePlugin.pluginName,\n      serviceTitle: selectedSharePlugin.title,\n      app: selectedSharePlugin.apps ? sharePlugin.app : undefined\n    });\n    updateFps(Math.min(originalFps, fpsHistory[formatName]), formatName);\n  };\n\n  useEffect(() => {\n    if (isLoading) {\n      return;\n    }\n\n    const firstFormat = formats[0];\n    const formatName = firstFormat.format;\n\n    setFormat(formatName);\n\n    const firstPlugin = firstFormat.plugins.find(plugin => plugin.pluginName !== '_openWith');\n\n    setSharePlugin(firstPlugin && {\n      pluginName: firstPlugin.pluginName,\n      serviceTitle: firstPlugin.title\n    });\n\n    updateFps(Math.min(originalFps, fpsHistory[formatName]), formatName);\n  }, [isLoading]);\n\n  useEffect(() => {\n    setWidth(metadata.width);\n    setHeight(metadata.height);\n  }, [metadata]);\n\n  useEffect(() => {\n    if (!editPlugin) {\n      return;\n    }\n\n    const newPlugin = editServices.find(service => service.pluginName === editPlugin.pluginName && service.title === editPlugin.title);\n    setEditPlugin(newPlugin);\n  }, [editServices]);\n\n  const setDimensions = (dimensions: {width: number; height: number}) => {\n    setWidth(dimensions.width);\n    setHeight(dimensions.height);\n  };\n\n  return {\n    width,\n    height,\n    format,\n    fps,\n    originalFps,\n    editPlugin,\n    formats,\n    editServices,\n    sharePlugin,\n    updateSharePlugin,\n    updateFps,\n    updateFormat,\n    setEditPlugin,\n    setDimensions\n  };\n};\n\nconst OptionsContainer = createContainer(useOptions);\n\nexport default OptionsContainer;\n"
  },
  {
    "path": "renderer/components/editor/video-controls-container.tsx",
    "content": "import {createContainer} from 'unstated-next';\nimport electron from 'electron';\nimport {useRef, useState, useEffect} from 'react';\n\nconst useVideoControls = () => {\n  const videoRef = useRef<HTMLVideoElement>();\n  const currentWindow = electron.remote.getCurrentWindow();\n  const wasPaused = useRef(true);\n  const transitioningPauseState = useRef<Promise<void>>();\n\n  const [hasStarted, setHasStarted] = useState(false);\n  const [isMuted, setIsMuted] = useState(false);\n  const [isPaused, setIsPaused] = useState(true);\n\n  const play = async () => {\n    if (videoRef.current?.paused) {\n      transitioningPauseState.current = videoRef.current.play();\n      try {\n        await transitioningPauseState.current;\n        setIsPaused(false);\n      } catch {}\n    }\n  };\n\n  const pause = async () => {\n    if (videoRef.current && !videoRef.current.paused) {\n      try {\n        await transitioningPauseState.current;\n      } catch {} finally {\n        videoRef.current.pause();\n        setIsPaused(true);\n      }\n    }\n  };\n\n  const mute = () => {\n    setIsMuted(true);\n    videoRef.current.muted = true;\n  };\n\n  const unmute = () => {\n    setIsMuted(false);\n    videoRef.current.muted = false;\n  };\n\n  const setVideoRef = (video: HTMLVideoElement) => {\n    videoRef.current = video;\n    setIsPaused(video.paused);\n\n    if (video.paused) {\n      play();\n    }\n  };\n\n  const videoProps = {\n    onCanPlayThrough: hasStarted ? undefined : () => {\n      setHasStarted(true);\n      if (currentWindow.isFocused()) {\n        play();\n      }\n    },\n    onLoadedData: () => {\n      const hasAudio = (videoRef.current as any).webkitAudioDecodedByteCount > 0 || Boolean(\n        (videoRef.current as any).audioTracks &&\n        (videoRef.current as any).audioTracks.length > 0\n      );\n\n      if (!hasAudio) {\n        mute();\n      }\n    },\n    onEnded: () => {\n      play();\n    }\n  };\n\n  useEffect(() => {\n    const blurListener = () => {\n      wasPaused.current = videoRef.current?.paused;\n      if (!wasPaused.current) {\n        pause();\n      }\n    };\n\n    const focusListener = () => {\n      if (!wasPaused.current) {\n        play();\n      }\n    };\n\n    currentWindow.addListener('blur', blurListener);\n    currentWindow.addListener('focus', focusListener);\n\n    return () => {\n      currentWindow.removeListener('blur', blurListener);\n      currentWindow.removeListener('focus', focusListener);\n    };\n  }, []);\n\n  return {\n    isPaused,\n    isMuted,\n    setVideoRef,\n    pause,\n    play,\n    mute,\n    unmute,\n    videoProps\n  };\n};\n\nconst VideoControlsContainer = createContainer(useVideoControls);\n\nexport default VideoControlsContainer;\n"
  },
  {
    "path": "renderer/components/editor/video-metadata-container.tsx",
    "content": "import {createContainer} from 'unstated-next';\nimport {useRef, useState} from 'react';\nimport {useShowWindow} from '../../hooks/use-show-window';\n\nconst useVideoMetadata = () => {\n  const videoRef = useRef<HTMLVideoElement>();\n\n  const [width, setWidth] = useState(0);\n  const [height, setHeight] = useState(0);\n  const [hasAudio, setHasAudio] = useState(false);\n  const [duration, setDuration] = useState(0);\n  useShowWindow(duration !== 0);\n\n  const setVideoRef = (video: HTMLVideoElement) => {\n    videoRef.current = video;\n  };\n\n  const videoProps = {\n    onLoadedMetadata: () => {\n      setWidth(videoRef.current?.videoWidth);\n      setHeight(videoRef.current?.videoHeight);\n      setDuration(videoRef.current?.duration);\n    },\n    onLoadedData: () => {\n      const hasAudio = (videoRef.current as any).webkitAudioDecodedByteCount > 0 || Boolean(\n        (videoRef.current as any).audioTracks &&\n        (videoRef.current as any).audioTracks.length > 0\n      );\n\n      if (!hasAudio) {\n        videoRef.current.muted = true;\n      }\n\n      setHasAudio(hasAudio);\n    }\n  };\n\n  return {\n    width,\n    height,\n    hasAudio,\n    duration,\n    setVideoRef,\n    videoProps\n  };\n};\n\nconst VideoMetadataContainer = createContainer(useVideoMetadata);\n\nexport default VideoMetadataContainer;\n\n"
  },
  {
    "path": "renderer/components/editor/video-player.tsx",
    "content": "import Video from './video';\nimport LeftControls from './controls/left';\nimport RightControls from './controls/right';\nimport PlayBar from './controls/play-bar';\n\nconst VideoPlayer = () => {\n  return (\n    <div className=\"container\">\n      <Video/>\n      <div className=\"video-controls\">\n        <div className=\"controls left\"><LeftControls/></div>\n        <div className=\"controls center\"><PlayBar/></div>\n        <div className=\"controls right\"><RightControls/></div>\n      </div>\n      <style jsx>{`\n        .container {\n          flex: 1;\n          display: flex;\n          position: relative;\n          background: #000;\n        }\n\n        .video-controls {\n          position: absolute;\n          width: 100%;\n          height: 64px;\n          bottom: -64px;\n          left: 0;\n          background-image: linear-gradient(-180deg,transparent,rgba(0, 0, 0, 0.4));\n          padding: 16px 0;\n          display: flex;\n          align-items: center;\n          transition: bottom 0.12s ease-in-out;\n          -webkit-app-region: no-drag;\n        }\n\n        .left,\n        .right {\n          width: 20%;\n        }\n\n        .center {\n          width: 60%;\n          align-items: center;\n        }\n\n        .controls {\n          height: 100%;\n          display: flex;\n        }\n      `}</style>\n    </div>\n  );\n};\n\nexport default VideoPlayer;\n\n// Import PropTypes from 'prop-types';\n// import React from 'react';\n// import classNames from 'classnames';\n\n// import Video from './video';\n// import LeftControls from './controls/left';\n// import RightControls from './controls/right';\n// import PlayBar from './controls/play-bar';\n\n// export default class VideoPlayer extends React.Component {\n//   render() {\n//     const {hover} = this.props;\n\n//     const className = classNames('video-controls', {hover});\n\n//     return (\n//       <div className=\"container\">\n//         <Video/>\n//         <div className={className}>\n//           <div className=\"controls left\"><LeftControls/></div>\n//           <div className=\"controls center\"><PlayBar hover={hover}/></div>\n//           <div className=\"controls right\"><RightControls/></div>\n//         </div>\n//         <style jsx>{`\n//           .container {\n//             flex: 1;\n//             display: flex;\n//             position: relative;\n//           }\n\n//           .video-controls {\n//             position: absolute;\n//             width: 100%;\n//             height: 64px;\n//             bottom: ${hover ? 0 : -64}px;\n//             left: 0;\n//             background-image: linear-gradient(-180deg,transparent,rgba(0, 0, 0, 0.4));\n//             padding: 16px 0;\n//             display: flex;\n//             align-items: center;\n//             transition: bottom 0.12s ease-in-out;\n//             -webkit-app-region: no-drag;\n//           }\n\n//           .left,\n//           .right {\n//             width: 20%;\n//           }\n\n//           .center {\n//             width: 60%;\n//             align-items: center;\n//           }\n\n//           .controls {\n//             height: 100%;\n//             display: flex;\n//           }\n//         `}</style>\n//       </div>\n//     );\n//   }\n// }\n\n// VideoPlayer.propTypes = {\n//   hover: PropTypes.bool\n// };\n"
  },
  {
    "path": "renderer/components/editor/video-time-container.tsx",
    "content": "import {createContainer} from 'unstated-next';\nimport {useRef, useState, useEffect} from 'react';\n\nconst useVideoTime = () => {\n  const videoRef = useRef<HTMLVideoElement>();\n\n  const [startTime, setStartTime] = useState(0);\n  const [endTime, setEndTime] = useState(0);\n  const [duration, setDuration] = useState(0);\n  const [currentTime, setCurrentTime] = useState(0);\n\n  const setVideoRef = (video: HTMLVideoElement) => {\n    videoRef.current = video;\n  };\n\n  const videoProps = {\n    onLoadedMetadata: () => {\n      if (duration === 0) {\n        setDuration(videoRef.current?.duration);\n        setEndTime(videoRef.current?.duration);\n      }\n    },\n    onEnded: () => {\n      updateTime(startTime);\n    }\n  };\n\n  const updateTime = (time: number, ignoreElement = false) => {\n    if (time >= endTime && !videoRef.current.paused) {\n      videoRef.current.currentTime = startTime;\n      setCurrentTime(startTime);\n    } else {\n      if (!ignoreElement) {\n        videoRef.current.currentTime = time;\n      }\n\n      setCurrentTime(time);\n    }\n  };\n\n  const updateStartTime = (time: number) => {\n    if (time < endTime) {\n      videoRef.current.currentTime = time;\n      setStartTime(time);\n      setCurrentTime(time);\n    }\n  };\n\n  const updateEndTime = (time: number) => {\n    if (time > startTime) {\n      videoRef.current.currentTime = time;\n      setEndTime(time);\n      setCurrentTime(time);\n    }\n  };\n\n  useEffect(() => {\n    if (!videoRef.current) {\n      return;\n    }\n\n    const interval = setInterval(() => {\n      updateTime(videoRef.current.currentTime ?? 0, true);\n    }, 1000 / 30);\n\n    return () => {\n      clearInterval(interval);\n    };\n  }, [startTime, endTime]);\n\n  return {\n    startTime,\n    endTime,\n    duration,\n    currentTime,\n    updateTime,\n    updateStartTime,\n    updateEndTime,\n    setVideoRef,\n    videoProps\n  };\n};\n\nconst VideoTimeContainer = createContainer(useVideoTime);\n\nexport default VideoTimeContainer;\n"
  },
  {
    "path": "renderer/components/editor/video.tsx",
    "content": "import {useRef, useEffect} from 'react';\nimport VideoTimeContainer from './video-time-container';\nimport VideoMetadataContainer from './video-metadata-container';\nimport VideoControlsContainer from './video-controls-container';\nimport useEditorWindowState from 'hooks/editor/use-editor-window-state';\nimport {ipcRenderer as ipc} from 'electron-better-ipc';\n\nconst getVideoProps = (propsArray: Array<React.DetailedHTMLProps<React.VideoHTMLAttributes<HTMLVideoElement>, HTMLVideoElement>>) => {\n  const handlers = new Map();\n\n  for (const props of propsArray) {\n    for (const [key, handler] of Object.entries(props)) {\n      if (!handlers.has(key)) {\n        handlers.set(key, []);\n      }\n\n      handlers.get(key).push(handler);\n    }\n  }\n\n  // eslint-disable-next-line unicorn/no-array-reduce\n  return [...handlers.entries()].reduce((acc, [key, handlerList]) => ({\n    ...acc,\n    [key]: () => {\n      for (const handler of handlerList) {\n        handler?.();\n      }\n    }\n  }), {});\n};\n\nconst Video = () => {\n  const videoRef = useRef<HTMLVideoElement>();\n  const {filePath} = useEditorWindowState();\n  const src = `file://${filePath}`;\n\n  const videoTimeContainer = VideoTimeContainer.useContainer();\n  const videoMetadataContainer = VideoMetadataContainer.useContainer();\n  const videoControlsContainer = VideoControlsContainer.useContainer();\n\n  useEffect(() => {\n    videoTimeContainer.setVideoRef(videoRef.current);\n    videoMetadataContainer.setVideoRef(videoRef.current);\n    videoControlsContainer.setVideoRef(videoRef.current);\n  }, []);\n\n  const videoProps = getVideoProps([\n    videoTimeContainer.videoProps,\n    videoMetadataContainer.videoProps,\n    videoControlsContainer.videoProps\n  ]);\n\n  const onContextMenu = async () => {\n    const video = videoRef.current;\n\n    if (!video) {\n      return;\n    }\n\n    const wasPaused = video.paused;\n\n    if (!wasPaused) {\n      await videoControlsContainer.pause();\n    }\n\n    const {Menu} = require('electron-util').api;\n    const menu = Menu.buildFromTemplate([{\n      label: 'Snapshot',\n      click: () => {\n        ipc.callMain('save-snapshot', video.currentTime);\n      }\n    }]);\n\n    menu.popup({\n      callback: () => {\n        if (!wasPaused) {\n          videoControlsContainer.play();\n        }\n      }\n    });\n  };\n\n  return (\n    <div onContextMenu={onContextMenu}>\n      <video ref={videoRef} preload=\"auto\" src={src} {...videoProps}/>\n      <style jsx>{`\n        video {\n          width: 100%;\n          height: 100%;\n          max-height: calc(100vh - 48px);\n        }\n      `}</style>\n    </div>\n  );\n};\n\nexport default Video;\n"
  },
  {
    "path": "renderer/components/exports/export.tsx",
    "content": "import electron from 'electron';\nimport React, {useCallback, useMemo} from 'react';\nimport classNames from 'classnames';\n\nimport IconMenu from '../icon-menu';\nimport {CancelIcon, MoreIcon} from '../../vectors';\nimport {Progress, ProgressSpinner} from './progress';\nimport useConversion from '../../hooks/editor/use-conversion';\nimport {ExportStatus} from '../../common/types';\nimport {useShowWindow} from '../../hooks/use-show-window';\nimport {MenuItemConstructorOptions} from 'electron/common';\n\nconst stopPropagation = event => event.stopPropagation();\n\nconst Export = ({id}: {id: string}) => {\n  const {state, isLoading, cancel, openInEditor, showInFolder, retry, copy} = useConversion(id);\n  useShowWindow(state?.id !== undefined);\n\n  const isCancelable = state?.status === ExportStatus.inProgress;\n  const isActionable = state?.filePath && !state?.disableOutputActions;\n  const canRetry = [ExportStatus.canceled, ExportStatus.failed].includes(state?.status);\n\n  const onCancel = () => {\n    cancel();\n  };\n\n  const onClick = () => {\n    showInFolder();\n  };\n\n  const fileNameClassName = classNames({\n    title: true,\n    disabled: !isActionable\n  });\n\n  const onDragStart = useCallback(event => {\n    event.preventDefault();\n    if (isActionable) {\n      electron.ipcRenderer.send('drag-export', state?.id);\n    }\n  }, [isActionable, state?.id]);\n\n  const template = useMemo(() => {\n    const menuTemplate: MenuItemConstructorOptions[] = [{\n      label: 'Open Original',\n      click: () => openInEditor()\n    }];\n\n    if (state?.canCopy) {\n      menuTemplate.unshift({\n        label: 'Copy',\n        click: () => copy()\n      }, {\n        type: 'separator'\n      });\n    }\n\n    if (canRetry) {\n      menuTemplate.unshift({\n        label: 'Retry',\n        click: () => retry()\n      }, {\n        type: 'separator'\n      });\n    }\n\n    return menuTemplate;\n  }, [canRetry, retry, openInEditor, state?.canCopy, copy]);\n\n  if (isLoading) {\n    return null;\n  }\n\n  return (\n    <div draggable className=\"export-container\" onClick={onClick} onDragStart={onDragStart}>\n      <div className=\"thumbnail\">\n        <div className=\"overlay\"/>\n        <div className=\"icon\" onClick={stopPropagation}>\n          {\n            isCancelable ?\n              <div className=\"icon\" onClick={onCancel}>\n                <CancelIcon fill=\"white\" hoverFill=\"white\" activeFill=\"white\"/>\n              </div> :\n              <IconMenu\n                fillParent\n                icon={MoreIcon}\n                fill=\"white\"\n                hoverFill=\"white\"\n                activeFill=\"white\"\n                template={template}\n              />\n          }\n        </div>\n        <div className=\"progress\">\n          {\n            state.status === ExportStatus.inProgress && (\n              state.progress === 0 ?\n                <ProgressSpinner/> :\n                <Progress percent={Math.min(state.progress, 1)}/>\n            )\n          }\n        </div>\n      </div>\n      <div className=\"details\">\n        <div className={fileNameClassName} title={state.titleWithFormat}>\n          {state.titleWithFormat}\n        </div>\n        <div className=\"subtitle\" title={state.error?.message}>{state.message}{state.error && ` - ${state.error.message}`}</div>\n      </div>\n      <style jsx>{`\n          .export-container {\n            height: 80px;\n            border-bottom: 1px solid var(--row-divider-color);\n            padding: 16px;\n            display: flex;\n            align-items: center;\n            box-sizing: border-box;\n          }\n\n          .thumbnail {\n            width: 48px;\n            height: 48px;\n            flex-shrink: 0;\n            background: url(${state.image}) no-repeat center;\n            background-size: cover;\n            border-radius: 4px;\n            margin-right: 16px;\n            position: relative;\n            overflow: hidden;\n          }\n\n          .overlay {\n            position: absolute;\n            width: 100%;\n            height: 100%;\n            background: var(--thumbnail-overlay-color);\n          }\n\n          .icon, .progress {\n            position: absolute;\n            top: 0;\n            left: 0;\n            width: 100%;\n            height: 100%;\n            display: flex;\n            justify-content: center;\n            align-items: center;\n          }\n\n          .details {\n            flex: 1;\n            width: 224px;\n          }\n\n          .title {\n            font-size: 12px;\n            width: 224px;\n            white-space: nowrap;\n            overflow: hidden;\n            text-overflow: ellipsis;\n            color: var(--title-color);\n          }\n\n          .disabled {\n            color: var(--switch-disabled-color);\n          }\n\n          .subtitle {\n            font-size: 12px;\n            color: var(--subtitle-color);\n            user-select: none;\n            width: 100%;\n            white-space: nowrap;\n            overflow: hidden;\n            text-overflow: ellipsis;\n          }\n\n          .export-container:hover {\n            background: var(--row-hover-color);\n          }\n\n          .export-container:hover .progress {\n            display: none;\n          }\n\n          .export-container:not(:hover) .icon {\n            display: none;\n          }\n        `}</style>\n    </div>\n  );\n};\n\nexport default Export;\n"
  },
  {
    "path": "renderer/components/exports/index.tsx",
    "content": "import React, {useMemo} from 'react';\n\nimport useExportsList from '../../hooks/exports/use-exports-list';\nimport Export from './export';\n\nconst Exports = () => {\n  const {state = []} = useExportsList();\n\n  const exportList = useMemo(() => state.reverse(), [state]);\n\n  return (\n    <div>\n      {\n        exportList.map(id => (\n          <Export\n            key={id}\n            id={id}/>\n        ))\n      }\n      <style jsx>{`\n            flex: 1;\n            overflow-y: auto;\n            background: var(--background-color);\n        `}</style>\n    </div>\n  );\n};\n\nexport default Exports;\n"
  },
  {
    "path": "renderer/components/exports/progress.tsx",
    "content": "import PropTypes from 'prop-types';\nimport React from 'react';\n\nimport {SpinnerIcon} from '../../vectors';\n\nexport const ProgressSpinner = () => (\n  <div className=\"container\">\n    <SpinnerIcon stroke=\"#fff\"/>\n    <style jsx>{`\n      .container {\n        width: 24px;\n        height: 24px;\n        animation: spin 3s linear infinite;\n      }\n\n      @keyframes spin {\n        0% {\n          transform: rotate(0deg);\n        }\n\n        50% {\n          transform: rotate(720deg);\n        }\n\n        100% {\n          transform: rotate(1080deg);\n        }\n      }\n    `}</style>\n  </div>\n);\n\nexport const Progress = ({percent}: {percent: number}) => {\n  const circumference = 12 * 2 * Math.PI;\n  const offset = circumference - (percent * circumference);\n\n  return (\n    <svg viewBox=\"0 0 24 24\">\n      <circle stroke=\"white\" strokeWidth=\"2\" fill=\"transparent\" cx=\"12\" cy=\"12\" r=\"12\"/>\n      <style jsx>{`\n          svg {\n            width: 24px;\n            height: 24px;\n            overflow: visible;\n            transform: rotate(-90deg);\n          }\n\n          circle {\n            stroke-dasharray: ${circumference} ${circumference};\n            stroke-dashoffset: ${offset};\n            ${percent === 0 ? '' : 'transition: stroke-dashoffset 0.35s;'}\n          }\n        `}</style>\n    </svg>\n  );\n};\n"
  },
  {
    "path": "renderer/components/icon-menu.tsx",
    "content": "import {MenuItemConstructorOptions} from 'electron';\nimport React, {FunctionComponent, useRef} from 'react';\nimport {SvgProps} from 'vectors/svg';\n\ntype MenuProps = {\n  onOpen: (options: {x: number; y: number}) => void;\n} | {\n  template: MenuItemConstructorOptions[];\n};\n\ntype IconMenuProps = SvgProps & MenuProps & {\n  icon: FunctionComponent<SvgProps>;\n  fillParent?: boolean;\n};\n\nconst IconMenu: FunctionComponent<IconMenuProps> = props => {\n  const {icon: Icon, fillParent, ...iconProps} = props;\n  const container = useRef(null);\n\n  const openMenu = () => {\n    const boundingRect = container.current.children[0].getBoundingClientRect();\n    const {bottom, left} = boundingRect;\n\n    if ('onOpen' in props) {\n      props.onOpen({\n        x: Math.round(left),\n        y: Math.round(bottom)\n      });\n    } else {\n      const {api} = require('electron-util');\n      const menu = api.Menu.buildFromTemplate(props.template);\n      menu.popup({\n        x: Math.round(left),\n        y: Math.round(bottom)\n      });\n    }\n  };\n\n  return (\n    <div ref={container} onClick={openMenu}>\n      <Icon {...iconProps}/>\n      <style jsx>{`\n          display: flex;\n          align-items: center;\n          justify-content: center;\n          width: ${fillParent ? '100%' : 'none'};\n          height: ${fillParent ? '100%' : 'none'}\n        `}</style>\n    </div>\n  );\n};\n\nexport default IconMenu;\n"
  },
  {
    "path": "renderer/components/keyboard-number-input.js",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport {handleInputKeyPress} from '../utils/inputs';\n\nclass KeyboardNumberInput extends React.Component {\n  constructor(props) {\n    super(props);\n    this.inputRef = React.createRef();\n  }\n\n  getRef = () => {\n    return this.inputRef;\n  };\n\n  render() {\n    const {onChange, min, max, ...rest} = this.props;\n\n    return (\n      <input {...rest} ref={this.inputRef} type=\"text\" onChange={onChange} onKeyDown={handleInputKeyPress(onChange, min, max)}/>\n    );\n  }\n}\n\nKeyboardNumberInput.propTypes = {\n  onKeyDown: PropTypes.elementType,\n  min: PropTypes.number,\n  max: PropTypes.number,\n  onChange: PropTypes.elementType\n};\n\nexport default KeyboardNumberInput;\n"
  },
  {
    "path": "renderer/components/preferences/categories/category.js",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\n\nclass Category extends React.Component {\n  render() {\n    return (\n      <div className=\"category\">\n        {this.props.children}\n        <style jsx>{`\n            .category {\n              overflow-y: auto;\n              width: 100%;\n              height: 100%;\n              flex-shrink: 0;\n            }\n        `}</style>\n      </div>\n    );\n  }\n}\n\nCategory.propTypes = {\n  children: PropTypes.oneOfType([\n    PropTypes.arrayOf(PropTypes.node),\n    PropTypes.node\n  ]).isRequired\n};\n\nexport default Category;\n"
  },
  {
    "path": "renderer/components/preferences/categories/general.js",
    "content": "import electron from 'electron';\nimport React from 'react';\nimport PropTypes from 'prop-types';\nimport tildify from 'tildify';\n\nimport {connect, PreferencesContainer} from '../../../containers';\n\nimport Item from '../item';\nimport Switch from '../item/switch';\nimport Button from '../item/button';\nimport Select from '../item/select';\nimport ShortcutInput from '../shortcut-input';\n\nimport Category from './category';\n\nclass General extends React.Component {\n  static defaultProps = {\n    audioDevices: [],\n    kapturesDir: '',\n    category: 'general'\n  };\n\n  state = {};\n\n  componentDidMount() {\n    this.setState({\n      showCursorSupported: electron.remote.require('macos-version').isGreaterThanOrEqualTo('10.13')\n    });\n  }\n\n  openKapturesDir = () => {\n    electron.shell.openPath(this.props.kapturesDir);\n  };\n\n  render() {\n    const {\n      kapturesDir,\n      openOnStartup,\n      allowAnalytics,\n      showCursor,\n      highlightClicks,\n      record60fps,\n      enableShortcuts,\n      loopExports,\n      toggleSetting,\n      toggleRecordAudio,\n      audioInputDeviceId,\n      setAudioInputDeviceId,\n      audioDevices,\n      recordAudio,\n      pickKapturesDir,\n      setOpenOnStartup,\n      updateShortcut,\n      toggleShortcuts,\n      category,\n      lossyCompression,\n      shortcuts,\n      shortcutMap\n    } = this.props;\n\n    const {showCursorSupported} = this.state;\n\n    const devices = audioDevices.map(device => ({\n      label: device.name,\n      value: device.id\n    }));\n\n    const kapturesDirPath = tildify(kapturesDir);\n    const tabIndex = category === 'general' ? 0 : -1;\n    const fpsOptions = [{label: '30 FPS', value: false}, {label: '60 FPS', value: true}];\n\n    return (\n      <Category>\n        {\n          showCursorSupported &&\n          <Item\n            key=\"showCursor\"\n            parentItem\n            title=\"Show cursor\"\n            subtitle=\"Display the mouse cursor in your Kaptures\"\n          >\n            <Switch\n              tabIndex={tabIndex}\n              checked={showCursor}\n              onClick={\n                () => {\n                  if (showCursor) {\n                    toggleSetting('highlightClicks', false);\n                  }\n\n                  toggleSetting('showCursor');\n                }\n              }/>\n          </Item>\n        }\n        {\n          showCursorSupported &&\n          <Item key=\"highlightClicks\" subtitle=\"Highlight clicks\">\n            <Switch\n              tabIndex={tabIndex}\n              checked={highlightClicks}\n              disabled={!showCursor}\n              onClick={() => toggleSetting('highlightClicks')}\n            />\n          </Item>\n        }\n        <Item\n          key=\"enableShortcuts\"\n          parentItem\n          title=\"Keyboard shortcuts\"\n          subtitle=\"Toggle and customise keyboard shortcuts\"\n          help=\"You can paste any valid Electron accelerator string like Command+Shift+5\"\n        >\n          <Switch tabIndex={tabIndex} checked={enableShortcuts} onClick={toggleShortcuts}/>\n        </Item>\n        {\n          enableShortcuts && Object.entries(shortcutMap).map(([key, title]) => (\n            <Item key={key} subtitle={title}>\n              <ShortcutInput\n                shortcut={shortcuts[key]}\n                tabIndex={tabIndex}\n                onChange={shortcut => updateShortcut(key, shortcut)}\n              />\n            </Item>\n          ))\n        }\n        <Item\n          key=\"loopExports\"\n          title=\"Loop exports\"\n          subtitle=\"Infinitely loop exports when supported\"\n        >\n          <Switch tabIndex={tabIndex} checked={loopExports} onClick={() => toggleSetting('loopExports')}/>\n        </Item>\n        <Item\n          key=\"recordAudio\"\n          parentItem\n          title=\"Audio recording\"\n          subtitle=\"Record audio from input device\"\n        >\n          <Switch\n            tabIndex={tabIndex}\n            checked={recordAudio}\n            onClick={toggleRecordAudio}/>\n        </Item>\n        {\n          recordAudio &&\n          <Item key=\"audioInputDeviceId\" subtitle=\"Select input device\">\n            <Select\n              tabIndex={tabIndex}\n              options={devices}\n              selected={audioInputDeviceId}\n              placeholder=\"Select Device\"\n              noOptionsMessage=\"No input devices\"\n              onSelect={setAudioInputDeviceId}/>\n          </Item>\n        }\n        <Item\n          key=\"record60fps\"\n          title=\"Capture frame rate\"\n          subtitle=\"Increased FPS impacts performance and file size\"\n        >\n          <Select\n            tabIndex={tabIndex}\n            options={fpsOptions}\n            selected={record60fps}\n            onSelect={value => toggleSetting('record60fps', value)}/>\n        </Item>\n        <Item\n          key=\"allowAnalytics\"\n          title=\"Allow analytics\"\n          subtitle=\"Help us improve Kap by sending anonymous usage stats\"\n        >\n          <Switch tabIndex={tabIndex} checked={allowAnalytics} onClick={() => toggleSetting('allowAnalytics')}/>\n        </Item>\n        <Item\n          key=\"openOnStartup\"\n          title=\"Start automatically\"\n          subtitle=\"Launch Kap on system startup\"\n        >\n          <Switch tabIndex={tabIndex} checked={openOnStartup} onClick={setOpenOnStartup}/>\n        </Item>\n        <Item\n          key=\"pickKapturesDir\"\n          title=\"Save to…\"\n          subtitle={kapturesDirPath}\n          tooltip={kapturesDir}\n          onSubtitleClick={this.openKapturesDir}\n        >\n          <Button tabIndex={tabIndex} title=\"Choose\" onClick={pickKapturesDir}/>\n        </Item>\n        <Item\n          key=\"lossyCompression\"\n          parentItem\n          title=\"Lossy GIF compression\"\n          subtitle=\"Smaller file size for a minor quality degradation.\"\n        >\n          <Switch\n            tabIndex={tabIndex}\n            checked={lossyCompression}\n            onClick={() => toggleSetting('lossyCompression')}\n          />\n        </Item>\n      </Category>\n    );\n  }\n}\n\nGeneral.propTypes = {\n  showCursor: PropTypes.bool,\n  highlightClicks: PropTypes.bool,\n  record60fps: PropTypes.bool,\n  enableShortcuts: PropTypes.bool,\n  toggleSetting: PropTypes.elementType.isRequired,\n  toggleRecordAudio: PropTypes.elementType.isRequired,\n  audioInputDeviceId: PropTypes.string,\n  setAudioInputDeviceId: PropTypes.elementType.isRequired,\n  audioDevices: PropTypes.array,\n  recordAudio: PropTypes.bool,\n  kapturesDir: PropTypes.string,\n  openOnStartup: PropTypes.bool,\n  allowAnalytics: PropTypes.bool,\n  loopExports: PropTypes.bool,\n  pickKapturesDir: PropTypes.elementType.isRequired,\n  setOpenOnStartup: PropTypes.elementType.isRequired,\n  updateShortcut: PropTypes.elementType.isRequired,\n  toggleShortcuts: PropTypes.elementType.isRequired,\n  category: PropTypes.string,\n  shortcutMap: PropTypes.object,\n  shortcuts: PropTypes.object,\n  lossyCompression: PropTypes.bool\n};\n\nexport default connect(\n  [PreferencesContainer],\n  ({\n    showCursor,\n    highlightClicks,\n    record60fps,\n    recordAudio,\n    enableShortcuts,\n    audioInputDeviceId,\n    audioDevices,\n    kapturesDir,\n    openOnStartup,\n    allowAnalytics,\n    loopExports,\n    category,\n    lossyCompression,\n    shortcuts,\n    shortcutMap\n  }) => ({\n    showCursor,\n    highlightClicks,\n    record60fps,\n    recordAudio,\n    enableShortcuts,\n    audioInputDeviceId,\n    audioDevices,\n    kapturesDir,\n    openOnStartup,\n    allowAnalytics,\n    loopExports,\n    category,\n    lossyCompression,\n    shortcuts,\n    shortcutMap\n  }),\n  ({\n    toggleSetting,\n    toggleRecordAudio,\n    setAudioInputDeviceId,\n    pickKapturesDir,\n    setOpenOnStartup,\n    updateShortcut,\n    toggleShortcuts\n  }) => ({\n    toggleSetting,\n    toggleRecordAudio,\n    setAudioInputDeviceId,\n    pickKapturesDir,\n    setOpenOnStartup,\n    updateShortcut,\n    toggleShortcuts\n  })\n)(General);\n"
  },
  {
    "path": "renderer/components/preferences/categories/index.js",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport {ipcRenderer as ipc} from 'electron-better-ipc';\n\nimport {connect, PreferencesContainer} from '../../../containers';\n\nimport General from './general';\nimport Plugins from './plugins';\n\nconst CATEGORIES = [\n  {\n    name: 'general',\n    Component: General\n  }, {\n    name: 'plugins',\n    Component: Plugins\n  }\n];\n\nclass Categories extends React.Component {\n  componentDidUpdate(previousProps) {\n    if (!previousProps.isMounted && this.props.isMounted) {\n      // Wait for the transitions to end\n      setTimeout(async () => ipc.callMain('preferences-ready'), 300);\n    }\n  }\n\n  render() {\n    const {category} = this.props;\n\n    const index = CATEGORIES.findIndex(({name}) => name === category);\n\n    return (\n      <div className=\"categories-container\">\n        <div className=\"switcher\"/>\n        {\n          CATEGORIES.map(\n            ({name, Component}) => (\n              <Component key={name}/>\n            )\n          )\n        }\n        <style jsx>{`\n            .categories-container {\n              flex: 1;\n              display: flex;\n              overflow-x: hidden;\n              background: var(--background-color);\n            }\n\n            .switcher {\n              margin-left: -${index * 100}%;\n              transition: margin 0.3s ease-in-out;\n            }\n        `}</style>\n      </div>\n    );\n  }\n}\n\nCategories.propTypes = {\n  category: PropTypes.string,\n  isMounted: PropTypes.bool\n};\n\nexport default connect(\n  [PreferencesContainer],\n  ({category, isMounted}) => ({category, isMounted})\n)(Categories);\n"
  },
  {
    "path": "renderer/components/preferences/categories/plugins/index.js",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\n\nimport {connect, PreferencesContainer} from '../../../../containers';\nimport {handleKeyboardActivation} from '../../../../utils/inputs';\nimport Category from '../category';\nimport Tab, {EmptyTab} from './tab';\n\nclass Plugins extends React.Component {\n  static defaultProps = {\n    pluginsInstalled: [],\n    pluginsFromNpm: [],\n    category: 'general'\n  };\n\n  render() {\n    const {\n      pluginsInstalled,\n      pluginsFromNpm,\n      pluginBeingInstalled,\n      pluginBeingUninstalled,\n      togglePlugin,\n      onTransitionEnd,\n      tab,\n      selectTab,\n      npmError,\n      fetchFromNpm,\n      openPluginsConfig,\n      category\n    } = this.props;\n\n    const tabIndex = category === 'plugins' ? 0 : -1;\n    const allPlugins = [\n      ...pluginsInstalled,\n      ...pluginsFromNpm\n    ].sort((a, b) => {\n      if (a.isCompatible !== b.isCompatible) {\n        return b.isCompatible - a.isCompatible;\n      }\n\n      return a.prettyName.localeCompare(b.prettyName);\n    });\n\n    return (\n      <Category>\n        <div className=\"container\">\n          <nav className=\"plugins-nav\">\n            <div\n              tabIndex={tabIndex}\n              className={tab === 'discover' ? 'selected' : ''}\n              onClick={() => selectTab('discover')}\n              onKeyDown={handleKeyboardActivation(() => selectTab('discover'))}\n            >\n              Discover\n            </div>\n            <div\n              tabIndex={tabIndex}\n              className={tab === 'installed' ? 'selected' : ''}\n              onClick={() => selectTab('installed')}\n              onKeyDown={handleKeyboardActivation(() => selectTab('installed'))}\n            >\n              Installed\n            </div>\n          </nav>\n          <div className=\"tab-container\">\n            <div className=\"switcher\"/>\n            <div className=\"tab\" id=\"discover\">\n              {\n                npmError ? (\n                  <EmptyTab\n                    showIcon\n                    title=\"Oops!\"\n                    subtitle=\"Something went wrong…\"\n                    link=\"Refresh\"\n                    onClick={fetchFromNpm}/>\n                ) : (\n                  <Tab\n                    tabIndex={tabIndex === 0 && tab === 'discover' ? 0 : -1}\n                    current={pluginBeingInstalled || pluginBeingUninstalled}\n                    plugins={allPlugins}\n                    openConfig={openPluginsConfig}\n                    disabled={Boolean(pluginBeingInstalled || pluginBeingUninstalled)}\n                    onTransitionEnd={onTransitionEnd}\n                    onClick={togglePlugin}/>\n                )\n              }\n            </div>\n            <div className=\"tab\" id=\"installed\">\n              {\n                pluginsInstalled.length === 0 ? (\n                  <EmptyTab\n                    showIcon\n                    title=\"No plugins yet\"\n                    subtitle=\"Customize Kap to your liking with plugins.\"\n                    link=\"Browse\"\n                    onClick={() => selectTab('discover')}/>\n                ) : (\n                  <Tab\n                    tabIndex={tabIndex === 0 && tab === 'installed' ? 0 : -1}\n                    disabled={Boolean(pluginBeingInstalled)}\n                    current={pluginBeingUninstalled}\n                    plugins={pluginsInstalled}\n                    openConfig={openPluginsConfig}\n                    onClick={togglePlugin}\n                    onTransitionEnd={onTransitionEnd}/>\n                )\n              }\n            </div>\n          </div>\n        </div>\n        <style jsx>{`\n          .container {\n            height: 100%;\n            width: 100%;\n            display: flex;\n            flex-direction: column;\n          }\n\n          .plugins-nav {\n            height: 4.8rem;\n            padding: 0 16px;\n            display: flex;\n            align-items: center;\n            box-shadow: 0 1px 0 0 var(--row-divider-color), inset 0 1px 0 0 #fff;\n            z-index: 10;\n          }\n\n          .plugins-nav div {\n            margin-right: 16px;\n            height: 100%;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n            padding-bottom: 2px;\n            font-size: 1.2rem;\n            color: var(--kap);\n            font-weight: 500;\n            width: 64px;\n            outline: none;\n          }\n\n          .plugins-nav div:focus {\n            border-bottom: 2px solid rgba(0, 0, 0, 0.3);\n            padding-bottom: 0;\n          }\n\n          .plugins-nav .selected {\n            border-bottom: 2px solid var(--kap);\n            padding-bottom: 0;\n          }\n\n          .plugins-nav .selected:focus {\n            border-bottom: 2px solid var(--kap);\n            padding-bottom: 0;\n          }\n\n          .tab-container {\n            flex: 1;\n            display: flex;\n            overflow-x: hidden;\n          }\n\n          .tab {\n            overflow-y: auto;\n            width: 100%;\n            height: 100%;\n            flex-shrink: 0;\n          }\n\n          .switcher {\n            margin-left: ${tab === 'discover' ? 0 : -100}%;\n            transition: margin 0.3s ease-in-out;\n          }\n        `}</style>\n      </Category>\n    );\n  }\n}\n\nPlugins.propTypes = {\n  pluginsInstalled: PropTypes.array,\n  pluginsFromNpm: PropTypes.array,\n  pluginBeingInstalled: PropTypes.string,\n  pluginBeingUninstalled: PropTypes.string,\n  togglePlugin: PropTypes.elementType.isRequired,\n  onTransitionEnd: PropTypes.elementType,\n  tab: PropTypes.string,\n  selectTab: PropTypes.elementType.isRequired,\n  npmError: PropTypes.bool,\n  fetchFromNpm: PropTypes.func.isRequired,\n  openPluginsConfig: PropTypes.func.isRequired,\n  category: PropTypes.string\n};\n\nexport default connect(\n  [PreferencesContainer],\n  ({\n    pluginsInstalled,\n    pluginsFromNpm,\n    pluginBeingInstalled,\n    pluginBeingUninstalled,\n    onTransitionEnd,\n    tab,\n    npmError,\n    category\n  }) => ({\n    pluginsInstalled,\n    pluginsFromNpm,\n    pluginBeingInstalled,\n    pluginBeingUninstalled,\n    onTransitionEnd,\n    tab,\n    npmError,\n    category\n  }), ({\n    togglePlugin,\n    selectTab,\n    fetchFromNpm,\n    openPluginsConfig\n  }) => ({\n    togglePlugin,\n    selectTab,\n    fetchFromNpm,\n    openPluginsConfig\n  })\n)(Plugins);\n"
  },
  {
    "path": "renderer/components/preferences/categories/plugins/plugin.js",
    "content": "import electron from 'electron';\nimport React from 'react';\nimport PropTypes from 'prop-types';\n\nimport Item from '../../item';\nimport Switch from '../../item/switch';\nimport {EditIcon, ErrorIcon} from '../../../../vectors';\n\nconst PluginTitle = ({title, label, onClick}) => (\n  <div>\n    <div\n      className=\"plugin-title\"\n      onClick={onClick}\n    >\n      {title}\n    </div>\n    <span>{label}</span>\n    <style jsx>{`\n      .plugin-title {\n        display: inline-block;\n        color: var(--kap);\n        cursor: pointer;\n      }\n\n      .plugin-title:hover {\n        text-decoration: underline;\n      }\n\n      span {\n        color: gray;\n        padding-left: 8px;\n      }\n    `}</style>\n  </div>\n);\n\nPluginTitle.propTypes = {\n  title: PropTypes.string,\n  label: PropTypes.string,\n  onClick: PropTypes.elementType\n};\n\nconst Plugin = ({plugin, checked, disabled, onTransitionEnd, onClick, loading, openConfig, tabIndex}) => {\n  const requiredVersion = !plugin.isCompatible && (\n    (plugin.kapVersion && `Requires Kap version ${plugin.kapVersion}.`) ||\n    (plugin.macosVersion && `Requires macOS version ${plugin.macosVersion}`)\n  );\n\n  const error = !plugin.isCompatible && (\n    <div className=\"invalid\" title={`This plugin is not supported. ${requiredVersion}`}>\n      <ErrorIcon fill=\"#ff6059\" hoverFill=\"#ff6059\" onClick={openConfig}/>\n      <style jsx>{`\n        .invalid {\n          height: 36px;\n          padding-right: 16px;\n          margin-right: 16px;\n          border-right: 1px solid var(--row-divider-color);\n          display: flex;\n          align-items: center;\n          justify-content: center;\n          align-self: center;\n        }\n      `}</style>\n    </div>\n  );\n\n  const warning = plugin.hasConfig && !plugin.isValid && (\n    <div className=\"invalid\" title=\"This plugin requires configuration\">\n      <ErrorIcon fill=\"#ff6059\" hoverFill=\"#ff6059\" onClick={openConfig}/>\n      <style jsx>{`\n        .invalid {\n          height: 36px;\n          padding-right: 16px;\n          margin-right: 16px;\n          border-right: 1px solid var(--row-divider-color);\n          display: flex;\n          align-items: center;\n          justify-content: center;\n          align-self: center;\n        }\n      `}</style>\n    </div>\n  );\n\n  const onTitleClick = () => {\n    if (plugin.link) {\n      electron.shell.openExternal(plugin.link);\n    }\n  };\n\n  return (\n    <Item\n      key={plugin.name}\n      warning={error || warning}\n      id={plugin.name}\n      title={\n        <PluginTitle\n          title={plugin.prettyName}\n          label={plugin.version}\n          onClick={onTitleClick}/>\n      }\n      subtitle={[plugin.description, requiredVersion].filter(Boolean)}\n    >\n      {\n        openConfig && plugin.isCompatible && (\n          <div className=\"config-icon\">\n            <EditIcon size=\"18px\" tabIndex={tabIndex} onClick={openConfig}/>\n            <style jsx>{`\n              .config-icon {\n                margin-right: 16px;\n                display: flex;\n              }\n            `}</style>\n          </div>\n        )\n      }\n      <Switch\n        tabIndex={tabIndex}\n        checked={checked}\n        disabled={disabled || (!plugin.isCompatible && !plugin.isInstalled) || plugin.isSymlink}\n        loading={loading}\n        onTransitionEnd={onTransitionEnd}\n        onClick={onClick}/>\n    </Item>\n  );\n};\n\nPlugin.propTypes = {\n  plugin: PropTypes.object,\n  checked: PropTypes.bool,\n  disabled: PropTypes.bool,\n  onTransitionEnd: PropTypes.elementType,\n  onClick: PropTypes.elementType,\n  loading: PropTypes.bool,\n  openConfig: PropTypes.func,\n  tabIndex: PropTypes.number.isRequired\n};\n\nexport default Plugin;\n"
  },
  {
    "path": "renderer/components/preferences/categories/plugins/tab.js",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\n\nimport Plugin from './plugin';\n\nexport const EmptyTab = ({title, subtitle, link, onClick, showIcon, image}) => {\n  return (\n    <div className=\"container\">\n      <div className=\"content\">\n        { showIcon && <div className=\"icon\">📦</div> }\n        <div className=\"title\">{title}</div>\n        <div className=\"subtitle\">{subtitle}</div>\n        <div className=\"link\" onClick={onClick}>{link}</div>\n      </div>\n      <footer/>\n      <style jsx>{`\n        .container {\n          height: 100%;\n          width: 100%;\n          display: flex;\n          flex-direction: column;\n          align-items: center;\n          justify-content: space-between;\n        }\n\n        .content {\n          display: flex;\n          flex-direction: column;\n          align-items: center;\n        }\n\n        .title {\n          height: 24px;\n          color: var(--title-color);\n          font-size: 1.6rem;\n          font-weight: 500;\n          margin-top: 36px;\n        }\n\n        .subtitle {\n          color: #808080;\n          font-size: 1.4rem;\n          font-weight: normal;\n          margin-bottom: 16px;\n        }\n\n        .link {\n          color: var(--kap);\n          font-size: 1.2rem;\n          font-weight: 500;\n        }\n\n        .icon {\n          font-size: 126px;\n          height: 20rem;\n          line-height: 20rem;\n          margin-bottom: -32px;\n        }\n\n        footer {\n          display: flex;\n          width: 100%;\n          ${image ? `background-image: url(${image});` : ''}\n          background-size: contain;\n          background-repeat: no-repeat;\n          background-position: center bottom;\n          height: 180px;\n        }\n      `}</style>\n    </div>\n  );\n};\n\nEmptyTab.propTypes = {\n  title: PropTypes.string,\n  subtitle: PropTypes.string,\n  link: PropTypes.string,\n  onClick: PropTypes.elementType.isRequired,\n  showIcon: PropTypes.bool,\n  image: PropTypes.string\n};\n\nconst Tab = ({current, plugins, disabled, onClick, onTransitionEnd, openConfig, tabIndex}) => {\n  return plugins.map(plugin => {\n    return (\n      <Plugin\n        key={plugin.name}\n        tabIndex={tabIndex}\n        plugin={plugin}\n        disabled={disabled}\n        loading={current === plugin.name}\n        checked={plugin.isInstalled ? (current !== plugin.name) : (current === plugin.name)}\n        openConfig={plugin.hasConfig ? (() => openConfig(plugin.name)) : undefined}\n        onClick={() => onClick(plugin)}\n        onTransitionEnd={onTransitionEnd}\n      />\n    );\n  });\n};\n\nTab.propTypes = {\n  checked: PropTypes.bool,\n  current: PropTypes.string,\n  plugins: PropTypes.array,\n  disabled: PropTypes.bool,\n  onClick: PropTypes.func.isRequired,\n  onTransitionEnd: PropTypes.func,\n  openConfig: PropTypes.func,\n  tabIndex: PropTypes.number.isRequired\n};\n\nexport default Tab;\n"
  },
  {
    "path": "renderer/components/preferences/item/button.js",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\n\nconst Button = ({title, onClick, tabIndex}) => (\n  <button type=\"button\" tabIndex={tabIndex} onClick={onClick}>\n    {title}\n    <style jsx>{`\n      border: 1px solid var(--input-border-color);\n      background: var(--input-background-color);\n      transition: border 0.12s ease-in-out;\n      height: 2.4rem;\n      padding: 0 0.8rem;\n      border-radius: 4px;\n      color: var(--title-color);\n      font-size: 1.2rem;\n      text-align: center;\n      line-height: 2.4rem;\n      outline: none;\n\n      :hover {\n        border-color: var(--input-hover-border-color);\n      }\n\n      :focus {\n        border-color: var(--kap);\n      }\n    `}</style>\n  </button>\n);\n\nButton.propTypes = {\n  title: PropTypes.string,\n  onClick: PropTypes.func.isRequired,\n  tabIndex: PropTypes.number.isRequired\n};\n\nexport default Button;\n"
  },
  {
    "path": "renderer/components/preferences/item/color-picker.js",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport classNames from 'classnames';\n\nconst ColorPicker = ({hasErrors, value, onChange}) => {\n  const className = classNames('container', {'has-errors': hasErrors});\n  const handleChange = event => {\n    const value = event.currentTarget.value.toUpperCase();\n    onChange(value.startsWith('#') ? value : `#${value}`);\n  };\n\n  return (\n    <div className={className}>\n      <div className=\"preview\">\n        <input\n          type=\"color\"\n          value={value}\n          onChange={handleChange}/>\n      </div>\n      <input\n        type=\"text\"\n        value={value.startsWith('#') ? value.slice(1, value.length) : value}\n        size={7}\n        onChange={handleChange}\n        onMouseUp={event => {\n          event.currentTarget.select();\n        }}\n      />\n      <style jsx>{`\n        .container {\n          display: flex;\n          align-items: center;\n          border: 1px solid var(--input-border-color);\n          background: var(--input-background-color);\n          transition: border 0.12s ease-in-out;\n          height: 2.4rem;\n          padding: 0 0.8rem;\n          border-radius: 4px;\n          color: var(--title-color);\n          font-size: 1.2rem;\n          text-align: center;\n          line-height: 2.4rem;\n          outline: none;\n        }\n\n        .has-errors {\n          border-color: rgba(255,59,48,0.20);\n        }\n\n        .container:hover {\n          border-color: var(--input-hover-border-color);\n        }\n  \n        .container:focus {\n          border-color: var(--kap);\n        }\n\n        .preview {\n          width: 10px;\n          height: 10px;\n          border-radius: 2px;\n          background-color: ${value};\n          margin-right: 8px;\n          position: relative;\n        }\n\n        input[type=\"text\"] {\n          outline: none;\n          border: none;\n          background: transparent;\n          color: var(--title-color);\n          font-size: 1.2rem;\n        }\n\n        input[type=\"color\"] {\n          opacity: 0;\n          z-index: 2;\n          position: absolute;\n          top: 0;\n          left: 0;\n          width: 100%;\n          height: 100%;\n        }\n      `}</style>\n    </div>\n  );\n};\n\nColorPicker.propTypes = {\n  value: PropTypes.string,\n  onChange: PropTypes.elementType,\n  hasErrors: PropTypes.bool\n};\n\nexport default ColorPicker;\n"
  },
  {
    "path": "renderer/components/preferences/item/index.js",
    "content": "import electron from 'electron';\nimport React from 'react';\nimport PropTypes from 'prop-types';\nimport classNames from 'classnames';\nimport Linkify from 'react-linkify';\nimport {HelpIcon} from '../../../vectors';\n\nexport const Link = ({href, children}) => (\n  <span onClick={async () => electron.shell.openExternal(href)}>\n    {children}\n    <style jsx>{`\n      color: var(--kap);\n      text-decoration: none;\n      cursor: pointer;\n\n      :hover {\n        text-decoration: underline;\n      }\n    `}</style>\n  </span>\n);\n\nLink.propTypes = {\n  href: PropTypes.string,\n  children: PropTypes.oneOfType([\n    PropTypes.arrayOf(PropTypes.node),\n    PropTypes.node\n  ])\n};\n\nclass Item extends React.Component {\n  static defaultProps = {\n    subtitle: [],\n    errors: []\n  };\n\n  render() {\n    const {\n      title,\n      subtitle,\n      experimental,\n      tooltip,\n      children,\n      id,\n      vertical,\n      errors,\n      onSubtitleClick,\n      warning,\n      onClick,\n      last,\n      parentItem,\n      small,\n      help\n    } = this.props;\n\n    const subtitleArray = Array.isArray(subtitle) ? subtitle : [subtitle];\n\n    const className = classNames('title', {experimental});\n    const containerClassName = classNames('container', {parent: parentItem});\n    const subtitleClassName = classNames('subtitle', {link: Boolean(onSubtitleClick)});\n\n    return (\n      <div className={containerClassName} onClick={onClick}>\n        <div className=\"item\" id={id}>\n          {warning}\n          <div className=\"content\">\n            <div className={className}>\n              {title}\n              {\n                help && (\n                  <div title={help}>\n                    <HelpIcon hoverFill=\"var(--icon-color)\" size=\"16px\"/>\n                  </div>\n                )\n              }\n            </div>\n            <div className={subtitleClassName} title={tooltip} onClick={onSubtitleClick}>\n              { subtitleArray.map(s => <div key={s}><Linkify component={Link}>{s}</Linkify></div>) }\n            </div>\n          </div>\n          <div className=\"input\">\n            {children}\n          </div>\n        </div>\n        {errors && errors.length > 0 && (\n          <div className=\"errors\">{errors.map(error => <div key={error}>{error}</div>)}</div>\n        )}\n        <style jsx>{`\n          .container {\n            display: flex;\n            max-width: 100%;\n            padding: ${small || onClick ? '16px' : '32px'} 16px;\n            margin-bottom: ${last ? '16px' : '0'};\n            border-bottom: 1px solid var(--row-divider-color);\n            flex-direction: column;\n          }\n\n          .parent {\n            padding-left: 0;\n            padding-right: 0;\n            margin-left: 16px;\n            margin-right: 16px;\n          }\n\n          .item {\n            display: flex;\n            flex-direction: ${vertical ? 'column' : 'row'};\n          }\n\n          .title {\n            font-size: 1.2rem;\n            line-height: 1.6rem;\n            font-weight: 500;\n            color: var(--title-color);\n            display: flex;\n          }\n\n          .title div {\n            margin-left: 8px;\n          }\n\n          .content {\n            flex: 1;\n            display: flex;\n            flex-direction: column;\n            justify-content: center;\n          }\n\n          .subtitle {\n            color: var(--${onClick ? 'link-color' : 'subtitle-color'});\n            font-weight: ${onClick ? '500' : 'normal'};\n            font-size: 1.2rem;\n            line-height: 1.6rem;\n            margin-top: 4px;\n            width: 100%;\n            padding-right: 16px;\n            box-sizing: border-box;\n          }\n\n          .input {\n            display: flex;\n            align-items: center;\n            margin-left: ${vertical ? '0px' : '8px'};\n          }\n\n          .experimental {\n            display: flex;\n            align-items: center;\n          }\n\n          .errors {\n            padding-top: 8px;\n            color: #ff6059;\n            font-size: 1.2rem;\n            line-height: 1.2rem;\n          }\n\n          .link {\n            color: var(--kap);\n            cursor: pointer;\n          }\n\n          .experimental:after {\n            border: 1px solid #ddd;\n            color: gray;\n            content: 'experimental';\n            display: inline-block;\n            font-size: 0.8rem;\n            font-weight: 500;\n            margin: 0 1rem;\n            border-radius: 3px;\n            padding: 3px 4px;\n            text-transform: uppercase;\n            width: max-content;\n            line-height: 1;\n          }\n        `}</style>\n      </div>\n    );\n  }\n}\n\nItem.propTypes = {\n  help: PropTypes.string,\n  id: PropTypes.string,\n  title: PropTypes.oneOfType([\n    PropTypes.string,\n    PropTypes.arrayOf(PropTypes.node),\n    PropTypes.node\n  ]),\n  experimental: PropTypes.bool,\n  tooltip: PropTypes.string,\n  subtitle: PropTypes.oneOfType([\n    PropTypes.string,\n    PropTypes.arrayOf(PropTypes.string)\n  ]),\n  children: PropTypes.oneOfType([\n    PropTypes.arrayOf(PropTypes.node),\n    PropTypes.node\n  ]),\n  vertical: PropTypes.bool,\n  errors: PropTypes.arrayOf(PropTypes.string),\n  onSubtitleClick: PropTypes.elementType,\n  warning: PropTypes.oneOfType([\n    PropTypes.arrayOf(PropTypes.node),\n    PropTypes.node\n  ]),\n  onClick: PropTypes.elementType,\n  last: PropTypes.bool,\n  parentItem: PropTypes.bool,\n  small: PropTypes.bool\n};\n\nexport default Item;\n"
  },
  {
    "path": "renderer/components/preferences/item/select.js",
    "content": "import electron from 'electron';\nimport PropTypes from 'prop-types';\nimport React from 'react';\n\nimport {DropdownArrowIcon} from '../../../vectors';\nimport {handleKeyboardActivation} from '../../../utils/inputs';\n\nclass Select extends React.Component {\n  static defaultProps = {\n    options: [],\n    placeholder: 'Select',\n    noOptionsMessage: 'No options'\n  };\n\n  constructor(props) {\n    super(props);\n    this.select = React.createRef();\n  }\n\n  state = {};\n\n  static getDerivedStateFromProps(nextProps) {\n    const {options, onSelect, selected} = nextProps;\n\n    if (!electron.remote || options.length === 0) {\n      return {};\n    }\n\n    const {Menu, MenuItem} = electron.remote;\n    const menu = new Menu();\n\n    for (const option of options) {\n      menu.append(\n        new MenuItem({\n          label: option.label,\n          type: 'radio',\n          checked: option.value === selected,\n          click: () => onSelect(option.value)\n        })\n      );\n    }\n\n    return {menu};\n  }\n\n  handleClick = () => {\n    if (this.props.options.length > 0) {\n      const boundingRect = this.select.current.getBoundingClientRect();\n\n      this.state.menu.popup({\n        x: Math.round(boundingRect.left),\n        y: Math.round(boundingRect.top)\n      });\n    }\n  };\n\n  render() {\n    const {options, selected, placeholder, noOptionsMessage, tabIndex, full} = this.props;\n\n    const selectedLabel = options.length === 0 ? noOptionsMessage : (\n      selected === undefined ? placeholder : options.find(option => option.value === selected).label\n    );\n\n    return (\n      <div\n        ref={this.select}\n        tabIndex={tabIndex}\n        className=\"select\"\n        onClick={this.handleClick}\n        onKeyDown={handleKeyboardActivation(this.handleClick, {isMenu: true})}\n      >\n        <span>{selectedLabel}</span>\n        <div className=\"dropdown\">\n          <DropdownArrowIcon size=\"15px\"/>\n        </div>\n        <style jsx>{`\n          .select {\n            background: var(--input-background-color);\n            border: 1px solid var(--input-border-color);\n            border-radius: 4px;\n            height: ${full ? '32px' : '2.4rem'};\n            transition: border 0.12s ease-in-out;\n            display: flex;\n            align-items: center;\n            padding-right: 32px;\n            user-select: none;\n            line-height: 2.4rem;\n            position: relative;\n            width: ${full ? '100%' : '92px'};\n            margin-top: ${full ? '8px' : '0px'};\n            color: var(--title-color);\n            outline: none;\n            box-shadow: var(--input-shadow);\n          }\n\n          .select:focus {\n            border-color: var(--kap);\n          }\n\n          .select span {\n            flex: 1;\n            padding-left: 8px;\n            font-size: 1.2rem;\n            overflow: hidden;\n            white-space: nowrap;\n            text-overflow: ellipsis;\n          }\n\n          .select:hover {\n            border-color: var(--input-hover-border-color);\n          }\n\n          .dropdown {\n            position: absolute;\n            top: 50%;\n            transform: translateY(-50%);\n            margin-top: -2px;\n            right: 8px;\n            pointer-events: none;\n            display: flex;\n          }\n        `}</style>\n      </div>\n    );\n  }\n}\n\nSelect.propTypes = {\n  options: PropTypes.arrayOf(PropTypes.shape({\n    label: PropTypes.string,\n    value: PropTypes.any\n  })),\n  onSelect: PropTypes.elementType.isRequired,\n  selected: PropTypes.any,\n  placeholder: PropTypes.string,\n  noOptionsMessage: PropTypes.string,\n  tabIndex: PropTypes.number.isRequired,\n  full: PropTypes.bool\n};\n\nexport default Select;\n"
  },
  {
    "path": "renderer/components/preferences/item/switch.js",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport classNames from 'classnames';\n\nimport {SpinnerIcon} from '../../../vectors';\nimport {handleKeyboardActivation} from '../../../utils/inputs';\n\nclass Switch extends React.Component {\n  render() {\n    const {checked, onClick, disabled, loading, onTransitionEnd, tabIndex} = this.props;\n    const className = classNames('switch', {checked, disabled, loading});\n\n    return (\n      <div\n        tabIndex={disabled ? -1 : tabIndex}\n        className={className}\n        onClick={disabled ? undefined : onClick}\n        onKeyDown={disabled ? undefined : handleKeyboardActivation(onClick)}\n      >\n        <div className=\"toggle\" onTransitionEnd={onTransitionEnd}>\n          {loading && <SpinnerIcon/>}\n        </div>\n        <style jsx>{`\n          .switch {\n            display: inline-block;\n            width: 4.8rem;\n            height: 2.4rem;\n            border: 1px solid var(--input-border-color);\n            border-radius: 2.625em;\n            position: relative;\n            background-color: var(--input-background-color);\n            transition: 0.2s ease-in-out;\n            box-sizing: border-box;\n            outline: none;\n            box-shadow: var(--switch-box-shadow);\n          }\n\n          .switch:not(.disabled):focus {\n            border-color: var(--kap);\n          }\n\n          .toggle {\n            content: '';\n            display: block;\n            width: 1.6rem;\n            height: 1.6rem;\n            border-radius: 50%;\n            margin-top: 0.3rem;\n            margin-left: 0.3rem;\n            position: absolute;\n            top: 0;\n            left: 0;\n            background: gray;\n            transition: left 0.12s ease-in-out;\n          }\n\n          .checked .toggle {\n            left: calc(100% - 2.2rem);\n            background: var(--kap);\n          }\n\n          .disabled {\n            cursor: not-allowed;\n          }\n\n          .disabled .toggle {\n            margin-top: 0.2rem;\n            border: 1px solid var(--switch-disabled-color);\n            background-color: transparent;\n          }\n\n          .loading .toggle {\n            border: none;\n            background: transparent;\n            background-size: 100%;\n            animation: spin 3s linear infinite;\n          }\n\n          @keyframes spin {\n            0% {\n              transform: rotate(0deg);\n            }\n\n            50% {\n              transform: rotate(720deg);\n            }\n\n            100% {\n              transform: rotate(1080deg);\n            }\n          }\n        `}</style>\n      </div>\n    );\n  }\n}\n\nSwitch.propTypes = {\n  checked: PropTypes.bool,\n  disabled: PropTypes.bool,\n  loading: PropTypes.bool,\n  onClick: PropTypes.func.isRequired,\n  onTransitionEnd: PropTypes.func,\n  tabIndex: PropTypes.number.isRequired\n};\n\nexport default Switch;\n"
  },
  {
    "path": "renderer/components/preferences/navigation.js",
    "content": "import classNames from 'classnames';\nimport React from 'react';\nimport PropTypes from 'prop-types';\n\nimport {connect, PreferencesContainer} from '../../containers';\nimport {SettingsIcon, PluginsIcon} from '../../vectors';\n\nimport {handleKeyboardActivation} from '../../utils/inputs';\n\nconst CATEGORIES = [\n  {\n    name: 'general',\n    icon: SettingsIcon\n  }, {\n    name: 'plugins',\n    icon: PluginsIcon\n  }\n];\n\nclass PreferencesNavigation extends React.Component {\n  static defaultProps = {\n    category: 'general'\n  };\n\n  render() {\n    const {selectCategory, category} = this.props;\n\n    return (\n      <nav className=\"prefs-nav\">\n        {\n          CATEGORIES.map(\n            ({name, icon: Icon}) => (\n              <div\n                key={name}\n                tabIndex={0}\n                className={classNames('nav-item', {active: category === name})}\n                onClick={() => selectCategory(name)}\n                onKeyDown={handleKeyboardActivation(() => selectCategory(name))}\n              >\n                <Icon\n                  size=\"2.4rem\"\n                  active={category === name}\n                />\n                <span>{name}</span>\n              </div>\n            )\n          )\n        }\n        <style jsx>{`\n          .prefs-nav {\n            height: 4.8rem;\n            padding: 0 16px;\n            display: flex;\n            align-items: center;\n          }\n\n          .nav-item {\n            display: flex;\n            align-items: center;\n            margin-right: 24px;\n            height: 24px;\n            color: var(--subtitle-color);\n            border-radius: 4px;\n            font-size: 12px;\n            font-weight: 500;\n            line-height: 16px;\n            text-transform: capitalize;\n            border: 1px solid transparent;\n            outline: none;\n            padding-right: 8px;\n          }\n\n          .nav-item.active {\n            color: var(--title-color);\n            border-color: var(--navigation-item-border-color);\n            outline: var(--navigation-item-outline-active);\n            box-shadow: var(--navigation-item-box-shadow-active);\n            background: var(--navigation-item-background);\n          }\n\n          .nav-item:focus {\n            border-color: var(--navigation-item-border-color);\n            box-shadow: var(--navigation-item-box-shadow);\n            background: var(--navigation-item-background);\n          }\n\n          .nav-item.active:focus {\n            border-color: var(--navigation-item-active-border-color);\n          }\n\n          .nav-item:hover {\n            --icon-color: var(--icon-hover-color);\n            color: var(--navigation-item-hover-color);\n          }\n\n          span {\n            margin-left: 8px;\n          }\n        `}</style>\n      </nav>\n    );\n  }\n}\n\nPreferencesNavigation.propTypes = {\n  category: PropTypes.string,\n  selectCategory: PropTypes.elementType.isRequired\n};\n\nexport default connect(\n  [PreferencesContainer],\n  ({category}) => ({category}),\n  ({selectCategory}) => ({selectCategory})\n)(PreferencesNavigation);\n"
  },
  {
    "path": "renderer/components/preferences/shortcut-input.js",
    "content": "import React, {useRef, useEffect, useState} from 'react';\nimport PropTypes from 'prop-types';\nimport classNames from 'classnames';\nimport {shake} from '../../utils/inputs';\nimport {checkAccelerator, eventKeyToAccelerator} from 'common/accelerator-validator';\nimport {DropdownArrowIcon} from '../../vectors';\n\nconst presets = [\n  'Command+Shift+3',\n  'Command+Shift+4',\n  'Command+Shift+5',\n  'Command+Shift+6'\n];\n\nconst Key = ({children}) => (\n  <span>\n    {children}\n    <style jsx>{`\n    span {\n      color: var(--title-color);\n      display: flex;\n      justify-content: center;\n      align-items: center;\n      font-weight: 500;\n      font-size: 12px;\n      background: var(--shortcut-key-background);\n      border-radius: 4px 4px 4px 4px;\n      border: 1px solid var(--shortcut-key-border);\n      height: 20px;\n      padding: 0 5px;\n      margin-right: 2px;\n      box-sizing: border-box;\n      box-shadow: var(--shortcut-box-shadow);\n      test-transform: uppercase;\n    }\n  `}</style>\n  </span>\n);\n\nKey.propTypes = {\n  children: PropTypes.oneOfType([\n    PropTypes.arrayOf(PropTypes.node),\n    PropTypes.node\n  ]).isRequired\n};\n\nconst metaCharacters = new Map([\n  ['Command', '⌘'],\n  ['Alt', '⌥'],\n  ['Option', '⌥'],\n  ['Shift', '⇧'],\n  ['Cmd', '⌘'],\n  ['Control', '⌃'],\n  ['Ctrl', '⌃']\n]);\n\nconst ShortcutInput = ({shortcut = '', onChange, tabIndex}) => {\n  const [keys, setKeys] = useState(shortcut.split('+').filter(Boolean));\n  const [isEditing, setIsEditing] = useState(false);\n  const boxRef = useRef();\n  const inputRef = useRef();\n\n  const resetKeys = () => {\n    setKeys(shortcut.split('+').filter(Boolean));\n  };\n\n  useEffect(() => {\n    resetKeys();\n  }, [shortcut]);\n\n  const keysToRender = keys.map(key => metaCharacters.get(key) || key);\n\n  const clearShortcut = () => {\n    setKeys([]);\n    onChange(undefined);\n  };\n\n  const cancel = event => {\n    const {metaKey, altKey, ctrlKey, shiftKey, key} = event;\n    const metaKeys = [\n      metaKey && 'Command',\n      altKey && 'Alt',\n      ctrlKey && 'Control',\n      shiftKey && 'Shift'\n    ].filter(Boolean);\n\n    if (metaKeys.length > 0 && ['Shift', 'Control', 'Alt', 'Meta'].includes(key)) {\n      setKeys(metaKeys);\n      return;\n    }\n\n    shake(boxRef.current);\n    resetKeys();\n    setIsEditing(false);\n  };\n\n  const handleKeyDown = event => {\n    // TODO: Use `code` instead of `keyCode` when this is released https://github.com/facebook/react/pull/18287\n    const {metaKey, altKey, ctrlKey, shiftKey, key, location, keyCode} = event;\n    const metaKeys = [\n      metaKey && 'Command',\n      altKey && 'Alt',\n      ctrlKey && 'Control',\n      shiftKey && 'Shift'\n    ].filter(Boolean);\n\n    if (metaKeys.length === 0) {\n      if (key === 'Tab') {\n        return;\n      }\n\n      if (['Escape', 'Delete', 'Backspace'].includes(key)) {\n        clearShortcut();\n        return;\n      }\n    }\n\n    // Handled by the `onPaste` event\n    if (metaKeys.length === 1 && metaKey && key.toUpperCase() === 'V') {\n      return;\n    }\n\n    if (['Shift', 'Control', 'Alt', 'Meta'].includes(key)) {\n      setKeys(metaKeys);\n      setIsEditing(true);\n      return;\n    }\n\n    const mappedKey = (keyCode > 47 && keyCode < 58) || (keyCode > 64 && keyCode < 91) ? String.fromCharCode(keyCode) : key;\n\n    const keys = [...metaKeys, eventKeyToAccelerator(mappedKey, location)];\n    const accelerator = keys.join('+');\n    setIsEditing(false);\n    if (checkAccelerator(accelerator)) {\n      setKeys(keys);\n      onChange(accelerator);\n    } else {\n      shake(boxRef.current);\n      resetKeys();\n    }\n  };\n\n  const paste = event => {\n    const text = (event.clipboardData || window.clipboardData).getData('text');\n\n    setIsEditing(false);\n    if (checkAccelerator(text)) {\n      setKeys(text.split('+').filter(Boolean));\n      onChange(text);\n    } else {\n      shake(boxRef.current);\n      resetKeys();\n    }\n  };\n\n  const openMenu = () => {\n    const {Menu} = require('electron').remote;\n    const menu = Menu.buildFromTemplate(presets.map(accelerator => ({\n      label: accelerator.split('+').map(key => metaCharacters.get(key) || key).join(''),\n      click: () => {\n        onChange(accelerator);\n      }\n    })));\n\n    const {left, top} = boxRef.current.getBoundingClientRect();\n    menu.popup({\n      x: Math.round(left),\n      y: Math.round(top)\n    });\n  };\n\n  const className = classNames('box', {invalid: false});\n\n  return (\n    <div className=\"shortcut-input\">\n      <div ref={boxRef} className={className} onClick={() => inputRef.current.focus()}>\n        <div className=\"key-container\">\n          {keysToRender.map(key => <Key key={key}>{key}</Key>)}\n          <input\n            ref={inputRef}\n            tabIndex={tabIndex}\n            onKeyDown={handleKeyDown}\n            onKeyUp={isEditing ? cancel : undefined}\n            onBlur={isEditing ? cancel : undefined}\n            onPaste={paste}\n          />\n        </div>\n        <div className=\"dropdown\">\n          <DropdownArrowIcon onClick={openMenu}/>\n        </div>\n      </div>\n      <button type=\"button\" tabIndex={tabIndex} onClick={clearShortcut}>\n        <svg style={{width: '20px', height: '20px'}} viewBox=\"0 0 24 24\">\n          <path fill=\"var(--icon-color)\" d=\"M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z\"/>\n        </svg>\n      </button>\n      <style jsx>{`\n        .shortcut-input {\n          display: flex;\n          flex-direction: row;\n          align-items: stretch;\n          justify-content: stretch;\n        }\n\n        .dropdown {\n          cursor: default;\n        }\n\n        .box {\n          position: relative;\n          padding: 1px 1px;\n          background: var(--input-background-color);\n          border-radius: 3px;\n          border: 1px solid var(--input-border-color);\n          min-width: 96px;\n          cursor: text;\n          display: flex;\n          height: 24px;\n          box-sizing: border-box;\n          justify-content: space-between;\n          align-items: center;\n        }\n\n        .key-container {\n          display: flex;\n        }\n\n        .box:focus-within {\n          border-color: var(--input-focus-border-color);\n        }\n\n        input {\n          display: inline-block;\n          width: 1px;\n          outline: none !important;\n          border: none;\n          background: transparent;\n          color: var(--title-color);\n        }\n\n        .invalid:focus-within {\n          border-color: red;\n        }\n\n        button {\n          display: inline-flex;\n          justify-content: center;\n          align-items: center;\n          background: var(--input-background-color);\n          border-radius: 3px 3px 3px 3px;\n          padding: 1px 3px;\n          border: 1px solid var(--input-border-color);\n          margin-left: 8px;\n          width: 24px;\n          height: 24px;\n          box-sizing: border-box;\n          outline: none;\n        }\n\n        button:focus {\n          border-color: var(--kap);\n        }\n\n        button:hover {\n          --icon-color: var(--navigation-item-hover-color);\n        }\n      `}</style>\n    </div>\n  );\n};\n\nShortcutInput.propTypes = {\n  shortcut: PropTypes.string,\n  onChange: PropTypes.func.isRequired,\n  tabIndex: PropTypes.number.isRequired\n};\n\nexport default ShortcutInput;\n"
  },
  {
    "path": "renderer/components/traffic-lights.tsx",
    "content": "import {remote} from 'electron';\nimport {useState, useEffect, FunctionComponent} from 'react';\n\ninterface TrafficLightsProps {\n  shouldClose?: () => PromiseLike<boolean>;\n}\n\nconst TrafficLights: FunctionComponent<TrafficLightsProps> = props => {\n  const currentWindow = remote.getCurrentWindow();\n  const [tint, setTint] = useState('blue');\n\n  useEffect(() => {\n    const setTintColor = () => {\n      setTint(remote.systemPreferences.getUserDefault('AppleAquaColorVariant', 'string') === '6' ? 'graphite' : 'blue');\n    };\n\n    const tintSubscription = remote.systemPreferences.subscribeNotification('AppleAquaColorVariantChanged', setTintColor);\n    setTintColor();\n\n    return () => {\n      remote.systemPreferences.unsubscribeNotification(tintSubscription);\n    };\n  }, []);\n\n  const enabled = {\n    close: currentWindow.closable,\n    minimize: currentWindow.minimizable,\n    maximize: currentWindow.maximizable\n  };\n\n  const getClassName = (name: string) => `traffic-light ${name}${enabled[name] ? '' : ' disabled'}`;\n\n  const close = async () => {\n    if (!props.shouldClose || await props.shouldClose()) {\n      currentWindow.close();\n    }\n  };\n\n  const minimize = () => {\n    currentWindow.minimize();\n  };\n\n  const maximize = () => {\n    currentWindow.setFullScreen(!currentWindow.isFullScreen());\n  };\n\n  return (\n    <div className={`traffic-lights ${tint}`}>\n      <div className={getClassName('close')} onClick={close}>\n        <svg width=\"12\" height=\"12\">\n          <circle cx=\"6\" cy=\"6\" r=\"5.75\" strokeWidth=\"0.5\"/>\n          <line x1=\"3.17\" y1=\"3.17\" x2=\"8.83\" y2=\"8.83\" stroke=\"black\"/>\n          <line x1=\"3.17\" y1=\"8.83\" x2=\"8.83\" y2=\"3.17\" stroke=\"#760e0e\"/>\n        </svg>\n      </div>\n      <div className={getClassName('minimize')} onClick={minimize}>\n        <svg width=\"12\" height=\"12\">\n          <circle cx=\"6\" cy=\"6\" r=\"5.75\" strokeWidth=\"0.5\"/>\n          <line x1=\"2\" y1=\"6\" x2=\"10\" y2=\"6\"/>\n        </svg>\n      </div>\n      <div className={getClassName('maximize')} onClick={maximize}>\n        <svg width=\"12\" height=\"12\">\n          <circle cx=\"6\" cy=\"6\" r=\"5.75\" strokeWidth=\"0.5\"/>\n          <rect x=\"3.5\" y=\"3.5\" width=\"5\" height=\"5\" rx=\"1\" ry=\"1\"/>\n          <rect className=\"background-rect\" x=\"5.5\" y=\"1.5\" width=\"1\" height=\"9\" transform=\"rotate(-45 6 6)\"/>\n        </svg>\n      </div>\n      <style jsx>{`\n          .traffic-lights {\n            display: flex;\n            align-items: center;\n            height: max-content;\n            margin-left: 12px;\n          }\n\n          .traffic-light {\n            border-radius: 100%;\n            height: 12px;\n            width: 12px;\n            background-color: #ddd;\n            margin-right: 8px;\n            position: relative;\n            -webkit-app-region: no-drag;\n          }\n\n          .traffic-light line,\n          .traffic-light rect {\n            visibility: hidden;\n          }\n\n          .traffic-lights:hover line,\n          .traffic-lights:hover rect {\n            visibility: visible;\n          }\n\n          .close line {\n            stroke: #580300;\n          }\n\n          .close circle {\n            stroke: #E24640;\n            fill: #ff6155;\n          }\n\n          .close:active circle {\n            fill: #c1483f;\n            stroke: #c1483f;\n          }\n\n          .close:active line {\n            stroke: #1e0101;\n          }\n\n          .minimize line {\n            stroke: #a66400;\n          }\n\n          .minimize circle {\n            stroke: #DFA023;\n            fill: #ffc008;\n          }\n\n          .minimize:active circle {\n            fill: #c0910a;\n            stroke: #c0910a;\n          }\n\n          .minimize:active line {\n            stroke: #5a2800;\n          }\n\n          .maximize rect {\n            fill: #006500;\n            stroke: #006500;\n          }\n\n          .maximize:active rect {\n            fill: #003200;\n            stroke: #003200;\n          }\n\n          .maximize circle {\n            stroke: #1BAC2C;\n            fill: #16d137;\n          }\n\n          .maximize .background-rect {\n            stroke: #16d137;\n            fill: #16d137;\n          }\n\n          .maximize:active circle,\n          .maximize:active .background-rect {\n            fill: #119b29;\n            stroke: #119b29;\n          }\n\n          .graphite .close circle,\n          .graphite .minimize circle,\n          .graphite .maximize circle {\n            fill: #8f8f94;\n            stroke: #606066;\n          }\n\n          .graphite .close line,\n          .graphite .minimize line {\n            stroke: #27272c;\n          }\n\n          .maximize rect {\n            fill: #27272c;\n            stroke: #27272c;\n          }\n\n          .graphite .maximize .background-rect {\n            fill: #8f8f94;\n            stroke: #8f8f94;\n          }\n\n          .traffic-light.disabled circle {\n            fill: #6b6c6d;\n            stroke: #6b6c6d;\n          }\n\n          .traffic-light.disabled line,\n          .traffic-light.disabled rect {\n            visibility: hidden;\n          }\n        `}</style>\n    </div>\n  );\n};\n\nexport default TrafficLights;\n"
  },
  {
    "path": "renderer/components/window-header.js",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\n\nclass WindowHeader extends React.Component {\n  render() {\n    return (\n      <header className=\"window-header\">\n        <span>{this.props.title}</span>\n        {this.props.children}\n        <style jsx>{`\n          .window-header {\n            min-height: 3.6rem;\n            position: relative;\n            display: flex;\n            flex-direction: column;\n            background: var(--window-header-background);\n            box-shadow: var(--window-header-box-shadow);\n            -webkit-app-region: drag;\n            user-select: none;\n            z-index: 11;\n          }\n\n          .window-header span {\n            line-height: 3.6rem;\n            font-size: 1.4rem;\n            text-align: center;\n            width: 100%;\n            color: var(--subtitle-color);\n          }\n        `}</style>\n      </header>\n    );\n  }\n}\n\nWindowHeader.propTypes = {\n  title: PropTypes.string,\n  children: PropTypes.oneOfType([\n    PropTypes.arrayOf(PropTypes.node),\n    PropTypes.node\n  ])\n};\n\nexport default WindowHeader;\n"
  },
  {
    "path": "renderer/containers/action-bar.js",
    "content": "import electron from 'electron';\nimport {Container} from 'unstated';\n\nconst barWidth = 464;\nconst barHeight = 64;\n\nexport default class ActionBarContainer extends Container {\n  remote = electron.remote || false;\n\n  constructor() {\n    super();\n\n    if (!this.remote) {\n      this.state = {};\n      return;\n    }\n\n    this.settings = this.remote.require('./common/settings').settings;\n    this.state = {\n      cropperWidth: '',\n      cropperHeight: ''\n    };\n  }\n\n  setInputValues = ({width, height}) => {\n    this.setState({\n      cropperWidth: width ? width.toString() : '',\n      cropperHeight: height ? height.toString() : ''\n    });\n  };\n\n  setWidth = cropperWidth => {\n    this.setState({cropperWidth});\n  };\n\n  setHeight = cropperHeight => {\n    this.setState({cropperHeight});\n  };\n\n  setDisplay = display => {\n    const {width, height, cropper} = display;\n    const {x, y, ratioLocked} = cropper ? this.settings.get('actionBar') : {};\n\n    this.setState({\n      screenWidth: width,\n      screenHeight: height,\n      x: x ? x : (width - barWidth) / 2,\n      y: y ? y : Math.ceil(height * 0.8),\n      width: barWidth,\n      height: barHeight,\n      ratioLocked\n    });\n  };\n\n  resetPosition = () => {\n    const {screenWidth, screenHeight} = this.state;\n\n    this.setState({\n      x: (screenWidth - barWidth) / 2,\n      y: Math.ceil(screenHeight * 0.8),\n      width: barWidth,\n      height: barHeight\n    });\n  };\n\n  bindCursor = cursorContainer => {\n    this.cursorContainer = cursorContainer;\n  };\n\n  bindCropper = cropperContainer => {\n    this.cropperContainer = cropperContainer;\n  };\n\n  updateSettings = updates => {\n    const {x, y, ratioLocked} = this.state;\n\n    this.settings.set('actionBar', {\n      x,\n      y,\n      ratioLocked,\n      ...updates\n    });\n    this.setState(updates);\n  };\n\n  toggleRatioLock = ratioLocked => {\n    const {ratioLocked: isLocked} = this.state;\n    if (ratioLocked) {\n      this.updateSettings({ratioLocked});\n    } else {\n      this.updateSettings({ratioLocked: !isLocked});\n    }\n\n    this.cropperContainer.setOriginal();\n  };\n\n  toggleAdvanced = () => {\n    if (!this.cropperContainer.state.isFullscreen) {\n      const {advanced, screenWidth} = this.state;\n      this.updateSettings({advanced: !advanced});\n\n      if (!advanced) {\n        const width = screenWidth * 0.2;\n        const height = width * 0.75; // 4:3\n        this.cropperContainer.setSize({\n          width,\n          height\n        });\n      }\n    }\n  };\n\n  startMoving = ({pageX, pageY}) => {\n    this.setState({isMoving: true, offsetX: pageX, offsetY: pageY});\n    this.cursorContainer.addCursorObserver(this.move);\n  };\n\n  stopMoving = () => {\n    const {x, y} = this.state;\n    this.updateSettings({x, y});\n    this.setState({isMoving: false});\n    this.cursorContainer.removeCursorObserver(this.move);\n  };\n\n  move = ({pageX, pageY}) => {\n    const {x, y, offsetX, offsetY, height, width, screenWidth, screenHeight} = this.state;\n\n    const updates = {\n      offsetX: pageX,\n      offsetY: pageY\n    };\n\n    if (y + pageY - offsetY + height <= screenHeight && y + pageY - offsetY >= 0) {\n      updates.y = y + pageY - offsetY;\n    }\n\n    if (x + pageX - offsetX + width <= screenWidth && x + pageX - offsetX >= 0) {\n      updates.x = x + pageX - offsetX;\n    }\n\n    this.setState(updates);\n  };\n}\n"
  },
  {
    "path": "renderer/containers/config.js",
    "content": "import electron from 'electron';\nimport {Container} from 'unstated';\n\nexport default class ConfigContainer extends Container {\n  remote = electron.remote || false;\n\n  state = {selectedTab: 0};\n\n  setPlugin(pluginName) {\n    const {InstalledPlugin} = this.remote.require('./plugins/plugin');\n    this.plugin = new InstalledPlugin(pluginName);\n    this.config = this.plugin.config;\n    this.validators = this.config.validators;\n    this.validate();\n    this.setState({\n      validators: this.validators,\n      values: this.config.store,\n      pluginName\n    });\n  }\n\n  setEditService = (pluginName, serviceTitle) => {\n    const {InstalledPlugin} = this.remote.require('./plugins/plugin');\n    this.plugin = new InstalledPlugin(pluginName);\n    this.config = this.plugin.config;\n    this.validators = this.config.validators.filter(({title}) => title === serviceTitle);\n    this.validate();\n    this.setState({\n      validators: this.validators,\n      values: this.config.store,\n      pluginName,\n      serviceTitle\n    });\n  };\n\n  validate = () => {\n    for (const validator of this.validators) {\n      validator.validate(this.config.store);\n    }\n  };\n\n  closeWindow = () => this.remote.getCurrentWindow().close();\n\n  openConfig = () => this.plugin.openConfigInEditor();\n\n  viewOnGithub = () => this.plugin.viewOnGithub();\n\n  onChange = (key, value) => {\n    if (value === undefined) {\n      this.config.delete(key);\n    } else {\n      this.config.set(key, value);\n    }\n\n    this.validate();\n    this.setState({values: this.config.store});\n  };\n\n  selectTab = selectedTab => {\n    this.setState({selectedTab});\n  };\n}\n"
  },
  {
    "path": "renderer/containers/cropper.js",
    "content": "import electron from 'electron';\nimport nearestNormalAspectRatio from 'nearest-normal-aspect-ratio';\nimport {Container} from 'unstated';\n\nimport {minHeight, minWidth, resizeTo, setScreenSize} from '../utils/inputs';\n\n// Helper function for retrieving the simplest ratio,\n// via the largest common divisor of two numbers (thanks @doot0)\nconst getLargestCommonDivisor = (first, second) => {\n  if (!first) {\n    return 1;\n  }\n\n  if (!second) {\n    return first;\n  }\n\n  return getLargestCommonDivisor(second, first % second);\n};\n\nconst getSimplestRatio = (width, height) => {\n  const lcd = getLargestCommonDivisor(width, height);\n  const denominator = width / lcd;\n  const numerator = height / lcd;\n\n  return [denominator, numerator];\n};\n\nexport const findRatioForSize = (width, height) => {\n  const ratio = nearestNormalAspectRatio(width, height);\n\n  if (ratio) {\n    return ratio.split(':').map(part => Number.parseInt(part, 10));\n  }\n\n  return getSimplestRatio(width, height);\n};\n\nexport default class CropperContainer extends Container {\n  remote = electron.remote || false;\n\n  constructor() {\n    super();\n\n    if (!this.remote) {\n      this.state = {};\n      return;\n    }\n\n    const {settings} = this.remote.require('./common/settings');\n    this.settings = settings;\n    this.settings.getSelectedInputDeviceId = this.remote.require('./utils/devices').getSelectedInputDeviceId;\n\n    this.state = {\n      isRecording: false,\n      isResizing: false,\n      isMoving: false,\n      isPicking: false,\n      resizeFromCenter: false,\n      showHandles: true,\n      selectedApp: '',\n      screenWidth: 0,\n      screenHeight: 0,\n      isActive: false,\n      isReady: false,\n      ratio: [1, 1],\n      recordAudio: this.settings.get('recordAudio'),\n      audioInputDeviceId: this.settings.getSelectedInputDeviceId()\n    };\n\n    this.settings.onDidChange('recordAudio', recordAudio => {\n      this.setState({recordAudio});\n    });\n\n    this.settings.onDidChange('audioInputDeviceId', async () => {\n      this.setState({audioInputDeviceId: this.settings.getSelectedInputDeviceId()});\n    });\n  }\n\n  setDisplay = display => {\n    const {width: screenWidth, height: screenHeight, isActive, id, cropper = {}} = display;\n    const {x, y, width, height, ratio = [4, 3]} = cropper;\n\n    setScreenSize(screenWidth, screenHeight);\n    this.setState({\n      screenWidth,\n      screenHeight,\n      isActive,\n      isReady: true,\n      displayId: id,\n      x: x || screenWidth / 2,\n      y: y || screenHeight / 2,\n      width,\n      height,\n      ratio\n    });\n    this.actionBarContainer.setInputValues({width, height});\n  };\n\n  willStartRecording = () => {\n    this.setState({willStartRecording: true});\n  };\n\n  setRecording = () => {\n    this.setState({isRecording: true});\n  };\n\n  setActive = isActive => {\n    const updates = {isActive};\n\n    if (!isActive) {\n      updates.x = 0;\n      updates.y = 0;\n      updates.width = 0;\n      updates.height = 0;\n      updates.isFullscreen = false;\n      updates.showHandles = true;\n      updates.selectedApp = '';\n    }\n\n    this.setState(updates);\n  };\n\n  updateSettings = updates => {\n    const {x, y, width, height, ratio, displayId} = this.state;\n\n    this.settings.set('cropper', {\n      x,\n      y,\n      width,\n      height,\n      ratio,\n      ...updates,\n      displayId\n    });\n    this.setState(updates);\n  };\n\n  setSize = ({width: defaultWidth, height: defaultHeight}) => {\n    let {width, height} = this.state;\n    width = width || defaultWidth;\n    height = height || defaultHeight;\n    const updates = {width, height};\n    this.settings.set('cropper', updates);\n    this.setState(updates);\n    this.actionBarContainer.setInputValues(updates);\n  };\n\n  bindCursor = cursorContainer => {\n    this.cursorContainer = cursorContainer;\n  };\n\n  bindActionBar = actionBarContainer => {\n    this.actionBarContainer = actionBarContainer;\n  };\n\n  setBounds = (bounds, {save = true, ignoreRatioLocked} = {}) => {\n    if (bounds) {\n      const updates = bounds;\n\n      if ((!this.actionBarContainer.state.ratioLocked || ignoreRatioLocked) && (bounds.width || bounds.height)) {\n        const {width, height} = this.state;\n        updates.ratio = findRatioForSize(bounds.width || width, bounds.height || height);\n      }\n\n      if (save) {\n        this.updateSettings(updates);\n      } else {\n        this.setState(updates);\n      }\n\n      this.actionBarContainer.setInputValues(updates);\n    } else if (this.state.width || this.state.height) {\n      this.actionBarContainer.setInputValues(this.state);\n    } else {\n      this.actionBarContainer.setInputValues({});\n    }\n  };\n\n  setRatio = ratio => {\n    const {x, y, width, screenHeight} = this.state;\n    const target = {width};\n\n    this.unselectApp();\n    const computedHeight = Math.ceil(width * ratio[1] / ratio[0]);\n    target.height = Math.max(minHeight, Math.min(screenHeight, computedHeight));\n\n    if (target.height !== computedHeight) {\n      target.width = Math.ceil(target.height * ratio[0] / ratio[1]);\n    }\n\n    const updates = {ratio, ...resizeTo({x, y}, target)};\n\n    this.updateSettings(updates);\n    this.actionBarContainer.setInputValues(updates);\n    this.actionBarContainer.toggleRatioLock(true);\n  };\n\n  swapDimensions = () => {\n    const {x, y, width, height, ratio, screenHeight} = this.state;\n    const target = {\n      width: height,\n      height: Math.min(width, screenHeight)\n    };\n\n    this.unselectApp();\n\n    if (target.height !== width) {\n      target.width = Math.ceil(target.height * ratio[1] / ratio[0]);\n    }\n\n    const updates = {ratio: ratio.reverse(), ...resizeTo({x, y}, target)};\n\n    this.updateSettings(updates);\n    this.actionBarContainer.setInputValues(updates);\n  };\n\n  selectApp = app => {\n    const {x, y, width, height, ownerName} = app;\n    this.setState({selectedApp: ownerName});\n    this.setBounds({x, y, width, height}, {ignoreRatioLocked: true});\n  };\n\n  unselectApp = () => {\n    if (this.state.selectedApp) {\n      this.setState({selectedApp: ''});\n    }\n  };\n\n  toggleResizeFromCenter = resizeFromCenter => {\n    this.setState({resizeFromCenter});\n  };\n\n  enterFullscreen = () => {\n    const {x, y, width, height, screenWidth, screenHeight} = this.state;\n    this.unselectApp();\n    this.setState({\n      isFullscreen: true,\n      x: 0,\n      y: 0,\n      width: screenWidth,\n      height: screenHeight,\n      showHandles: false,\n      original: {x, y, width, height}\n    });\n  };\n\n  exitFullscreen = () => {\n    const {original} = this.state;\n    this.setState({isFullscreen: false, showHandles: true, ...original});\n  };\n\n  startPicking = ({pageX, pageY}) => {\n    this.unselectApp();\n    this.setState({isPicking: true, original: {pageX, pageY}});\n    this.cursorContainer.addCursorObserver(this.pick);\n  };\n\n  pick = ({pageX, pageY}) => {\n    const {original, isPicking} = this.state;\n    const width = Math.abs(original.pageX - pageX);\n    const height = Math.abs(original.pageY - pageY);\n    if ((width > 0 || height > 0) && isPicking) {\n      this.cursorContainer.removeCursorObserver(this.pick);\n      const top = pageY < original.pageY;\n      const left = pageX < original.pageX;\n      this.setState({\n        x: Math.min(pageX, original.pageX),\n        y: Math.min(pageY, original.pageY),\n        width,\n        height,\n        isResizing: true,\n        isPicking: false,\n        currentHandle: {top, bottom: !top, left, right: !left}\n      });\n\n      this.setOriginal();\n      this.cursorContainer.addCursorObserver(this.resize);\n    }\n  };\n\n  stopPicking = () => {\n    if (this.state.isPicking) {\n      this.remote.getCurrentWindow().close();\n    } else {\n      this.cursorContainer.removeCursorObserver(this.pick);\n    }\n  };\n\n  setOriginal = () => {\n    const {x, y, width, height} = this.state;\n    this.setState({original: {x, y, width, height}});\n  };\n\n  startResizing = currentHandle => {\n    if (!this.state.isFullscreen) {\n      this.unselectApp();\n      this.setOriginal();\n      this.setState({currentHandle, isResizing: true});\n      this.cursorContainer.addCursorObserver(this.resize);\n    }\n  };\n\n  stopResizing = () => {\n    if (!this.state.isFullscreen && this.state.isResizing) {\n      const {x, y, width, height, ratio} = this.state;\n      this.setState({currentHandle: null, isResizing: false, showHandles: true, isPicking: false});\n      this.cursorContainer.removeCursorObserver(this.resize);\n      this.setBounds({\n        ...resizeTo({x, y}, {\n          width: Math.max(minWidth, width),\n          height: Math.max(minHeight, height)\n        }),\n        ratio\n      });\n    }\n  };\n\n  startMoving = ({pageX, pageY}) => {\n    if (!this.state.isFullscreen) {\n      this.unselectApp();\n      this.setState({isMoving: true, showHandles: false, offsetX: pageX, offsetY: pageY});\n      this.cursorContainer.addCursorObserver(this.move);\n    }\n  };\n\n  stopMoving = () => {\n    if (!this.state.isFullscreen && this.state.isMoving) {\n      const {x, y, width, height} = this.state;\n      this.setBounds({x, y, width, height});\n      this.setState({isMoving: false, showHandles: true});\n      this.cursorContainer.removeCursorObserver(this.move);\n      this.updateSettings({x, y});\n    }\n  };\n\n  move = ({pageX, pageY}) => {\n    const {x, y, offsetX, offsetY, width, height, screenWidth, screenHeight} = this.state;\n\n    const updates = {\n      offsetY: pageY,\n      offsetX: pageX\n    };\n\n    if (y + pageY - offsetY + height <= screenHeight && y + pageY - offsetY >= 0) {\n      updates.y = y + pageY - offsetY;\n    }\n\n    if (x + pageX - offsetX + width <= screenWidth && x + pageX - offsetX >= 0) {\n      updates.x = x + pageX - offsetX;\n    }\n\n    this.setBounds(updates, {save: false});\n  };\n\n  resize = ({pageX, pageY}) => {\n    const {currentHandle, x, y, width, height, original, ratio, screenWidth, screenHeight, resizeFromCenter} = this.state;\n    const {top, bottom, left, right} = currentHandle;\n    const {ratioLocked} = this.actionBarContainer.state;\n    const updates = {currentHandle: {top, bottom, right, left}};\n\n    if (top) {\n      updates.y = pageY;\n      updates.height = height + y - pageY;\n\n      if (resizeFromCenter) {\n        updates.height = Math.min((2 * (screenHeight - y)) - height, updates.height + y - pageY);\n        updates.y = y - ((updates.height - height) / 2);\n      }\n    } else if (bottom) {\n      updates.height = pageY - y;\n      updates.y = y;\n\n      if (resizeFromCenter) {\n        updates.y = Math.max(0, y + height - updates.height);\n        updates.height = height + (2 * (y - updates.y));\n      }\n    }\n\n    if (updates.height !== undefined && updates.height < 0 && !ratioLocked) {\n      updates.y += updates.height;\n      updates.height = -updates.height;\n      updates.currentHandle.bottom = !bottom;\n      updates.currentHandle.top = !top;\n    }\n\n    if (left) {\n      updates.x = pageX;\n      updates.width = width + x - pageX;\n\n      if (resizeFromCenter) {\n        updates.width = Math.min((2 * (screenWidth - x)) - width, updates.width + x - pageX);\n        updates.x = x - ((updates.width - width) / 2);\n      }\n    } else if (right) {\n      updates.width = pageX - x;\n      updates.x = x;\n\n      if (resizeFromCenter) {\n        updates.x = Math.max(0, x + width - updates.width);\n        updates.width = width + (2 * (x - updates.x));\n      }\n    }\n\n    if (updates.width !== undefined && updates.width < 0 && !ratioLocked) {\n      updates.x += updates.width;\n      updates.width = -updates.width;\n      updates.currentHandle.left = !left;\n      updates.currentHandle.right = !right;\n    }\n\n    if (ratioLocked) {\n      if (updates.width < 0 && updates.height < 0) {\n        updates.currentHandle = {bottom: !bottom, top: !top, left: !left, right: !right};\n      }\n\n      // Check which dimension has changed the most\n      if (\n        (updates.width - original.width) * ratio[1] > (updates.height - original.height) * ratio[0]\n      ) {\n        let lockedHeight = Math.ceil(updates.width * ratio[1] / ratio[0]);\n\n        if (resizeFromCenter) {\n          updates.y += (updates.height - lockedHeight) / 2;\n\n          if (updates.y < 0 || updates.y + lockedHeight > screenHeight) {\n            if (updates.y < 0) {\n              lockedHeight += updates.y * 2;\n              updates.y = 0;\n            } else {\n              lockedHeight -= (lockedHeight - (screenHeight - updates.y)) * 2;\n              updates.y = screenHeight - lockedHeight;\n            }\n\n            const lockedWidth = Math.ceil(lockedHeight * ratio[0] / ratio[1]);\n\n            updates.x += (updates.width - lockedWidth) / 2;\n            updates.width = lockedWidth;\n          }\n        } else if (top) {\n          updates.y += updates.height - lockedHeight;\n\n          if (updates.y < 0) {\n            lockedHeight += updates.y;\n            const lockedWidth = Math.ceil(lockedHeight * ratio[0] / ratio[1]);\n\n            updates.y = 0;\n\n            if (left) {\n              updates.x += updates.width - lockedWidth;\n            }\n\n            updates.width = lockedWidth;\n          }\n        } else if (updates.y + lockedHeight > screenHeight) {\n          lockedHeight = screenHeight - updates.y;\n          const lockedWidth = Math.ceil(lockedHeight * ratio[0] / ratio[1]);\n\n          if (left) {\n            updates.x += updates.width - lockedWidth;\n          }\n\n          updates.width = lockedWidth;\n        }\n\n        updates.height = lockedHeight;\n      } else {\n        let lockedWidth = Math.ceil(updates.height * ratio[0] / ratio[1]);\n\n        if (resizeFromCenter) {\n          updates.x += (updates.width - lockedWidth) / 2;\n\n          if (updates.x < 0 || updates.x + lockedWidth > screenWidth) {\n            if (updates.x < 0) {\n              lockedWidth += updates.x * 2;\n              updates.x = 0;\n            } else {\n              lockedWidth -= (lockedWidth - (screenWidth - updates.x)) * 2;\n              updates.x = screenWidth - lockedWidth;\n            }\n\n            const lockedHeight = Math.ceil(lockedWidth * ratio[1] / ratio[0]);\n\n            updates.y += (updates.height - lockedHeight) / 2;\n            updates.height = lockedHeight;\n          }\n        } else if (left) {\n          updates.x += updates.width - lockedWidth;\n\n          if (updates.x < 0) {\n            lockedWidth += updates.x;\n            const lockedHeight = Math.ceil(lockedWidth * ratio[1] / ratio[0]);\n\n            updates.x = 0;\n\n            if (top) {\n              updates.y += updates.height - lockedHeight;\n            }\n\n            updates.height = lockedHeight;\n          }\n        } else if (updates.x + lockedWidth > screenWidth) {\n          lockedWidth = screenWidth - updates.x;\n          const lockedHeight = Math.ceil(lockedWidth * ratio[1] / ratio[0]);\n\n          if (top) {\n            updates.y += updates.height - lockedHeight;\n          }\n\n          updates.height = lockedHeight;\n        }\n\n        updates.width = lockedWidth;\n      }\n    }\n\n    this.setBounds(updates, {save: false});\n  };\n}\n"
  },
  {
    "path": "renderer/containers/cursor.js",
    "content": "import {Container} from 'unstated';\n\nexport default class CursorContainer extends Container {\n  state = {\n    observers: []\n  };\n\n  setCursor = ({pageX, pageY}) => {\n    this.setState({cursorX: pageX, cursorY: pageY});\n    for (const observer of this.state.observers) {\n      observer({pageX, pageY});\n    }\n  };\n\n  addCursorObserver = observer => {\n    const {observers} = this.state;\n    this.setState({observers: [observer, ...observers]});\n  };\n\n  removeCursorObserver = observer => {\n    const {observers} = this.state;\n    this.setState({observers: observers.filter(o => o !== observer)});\n  };\n}\n"
  },
  {
    "path": "renderer/containers/index.js",
    "content": "import React from 'react';\nimport {Subscribe} from 'unstated';\n\nimport CropperContainer from './cropper';\nimport CursorContainer from './cursor';\nimport ActionBarContainer from './action-bar';\nimport PreferencesContainer from './preferences';\nimport ConfigContainer from './config';\n\nexport const connect = (containers, mapStateToProps, mapActionsToProps) => Component => props => (\n  <Subscribe to={containers}>\n    {\n      (...containers) => {\n        const stateProps = mapStateToProps ? mapStateToProps(...containers.map(a => a.state)) : {};\n        const actionProps = mapActionsToProps ? mapActionsToProps(...containers) : {};\n        const componentProps = {...props, ...stateProps, ...actionProps};\n\n        return <Component {...componentProps}/>;\n      }\n    }\n  </Subscribe>\n);\n\nexport {\n  CropperContainer,\n  CursorContainer,\n  ActionBarContainer,\n  PreferencesContainer,\n  ConfigContainer\n};\n"
  },
  {
    "path": "renderer/containers/preferences.js",
    "content": "import electron from 'electron';\nimport {Container} from 'unstated';\nimport {ipcRenderer as ipc} from 'electron-better-ipc';\n// Import {defaultInputDeviceId} from 'common/constants';\n\nconst defaultInputDeviceId = 'asd';\n\nconst SETTINGS_ANALYTICS_BLACKLIST = ['kapturesDir'];\n\nexport default class PreferencesContainer extends Container {\n  remote = electron.remote || false;\n\n  state = {\n    category: 'general',\n    tab: 'discover',\n    isMounted: false\n  };\n\n  mount = async setOverlay => {\n    this.setOverlay = setOverlay;\n    const {settings, shortcuts} = this.remote.require('./common/settings');\n    this.settings = settings;\n    this.settings.shortcuts = shortcuts;\n    this.systemPermissions = this.remote.require('./common/system-permissions');\n    this.plugins = this.remote.require('./plugins').plugins;\n    this.track = this.remote.require('./common/analytics').track;\n    this.showError = this.remote.require('./utils/errors').showError;\n\n    const pluginsInstalled = this.plugins.installedPlugins.sort((a, b) => a.prettyName.localeCompare(b.prettyName));\n\n    this.fetchFromNpm();\n\n    this.setState({\n      shortcuts: {},\n      ...this.settings.store,\n      openOnStartup: this.remote.app.getLoginItemSettings().openAtLogin,\n      pluginsInstalled,\n      isMounted: true,\n      shortcutMap: this.settings.shortcuts\n    });\n\n    if (this.settings.store.recordAudio) {\n      this.getAudioDevices();\n    }\n  };\n\n  getAudioDevices = async () => {\n    const {getAudioDevices, getDefaultInputDevice} = this.remote.require('./utils/devices');\n    const {audioInputDeviceId} = this.settings.store;\n    const {name: currentDefaultName} = getDefaultInputDevice() || {};\n\n    const audioDevices = await getAudioDevices();\n    const updates = {\n      audioDevices: [\n        {name: `System Default${currentDefaultName ? ` (${currentDefaultName})` : ''}`, id: defaultInputDeviceId},\n        ...audioDevices\n      ],\n      audioInputDeviceId\n    };\n\n    if (!audioDevices.some(device => device.id === audioInputDeviceId)) {\n      updates.audioInputDeviceId = defaultInputDeviceId;\n      this.settings.set('audioInputDeviceId', defaultInputDeviceId);\n    }\n\n    this.setState(updates);\n  };\n\n  scrollIntoView = (tabId, pluginId) => {\n    const plugin = document.querySelector(`#${tabId} #${pluginId}`).parentElement;\n    plugin.scrollIntoView({\n      behavior: 'smooth',\n      block: 'start',\n      inline: 'nearest'\n    });\n  };\n\n  openTarget = target => {\n    const isInstalled = this.state.pluginsInstalled.some(plugin => plugin.name === target.name);\n    const isFromNpm = this.state.pluginsFromNpm && this.state.pluginsFromNpm.some(plugin => plugin.name === target.name);\n\n    if (target.action === 'install') {\n      if (isInstalled) {\n        this.scrollIntoView(this.state.tab, target.name);\n        this.setState({category: 'plugins'});\n      } else if (isFromNpm) {\n        this.scrollIntoView('discover', target.name);\n        this.setState({category: 'plugins', tab: 'discover'});\n\n        const buttonIndex = this.remote.dialog.showMessageBoxSync(this.remote.getCurrentWindow(), {\n          type: 'question',\n          buttons: [\n            'Install',\n            'Cancel'\n          ],\n          defaultId: 0,\n          cancelId: 1,\n          message: `Do you want to install the “${target.name}” plugin?`\n        });\n\n        if (buttonIndex === 0) {\n          this.install(target.name);\n        }\n      } else {\n        this.setState({category: 'plugins'});\n      }\n    } else if (target.action === 'configure' && isInstalled) {\n      this.openPluginsConfig(target.name);\n    } else {\n      this.setState({category: 'plugins'});\n    }\n  };\n\n  setNavigation = ({category, tab, target}) => {\n    if (target) {\n      if (this.state.isMounted) {\n        this.openTarget(target);\n      } else {\n        this.setState({target});\n      }\n    } else {\n      this.setState({category, tab});\n    }\n  };\n\n  fetchFromNpm = async () => {\n    try {\n      const plugins = await this.plugins.getFromNpm();\n      this.setState({\n        npmError: false,\n        pluginsFromNpm: plugins.sort((a, b) => {\n          if (a.isCompatible !== b.isCompatible) {\n            return b.isCompatible - a.isCompatible;\n          }\n\n          return a.prettyName.localeCompare(b.prettyName);\n        })\n      });\n\n      if (this.state.target) {\n        this.openTarget(this.state.target);\n        this.setState({target: undefined});\n      }\n    } catch {\n      this.setState({npmError: true});\n    }\n  };\n\n  togglePlugin = plugin => {\n    if (plugin.isInstalled) {\n      this.uninstall(plugin.name);\n    } else {\n      this.install(plugin.name);\n    }\n  };\n\n  install = async name => {\n    const {pluginsInstalled, pluginsFromNpm} = this.state;\n\n    this.setState({pluginBeingInstalled: name});\n    const result = await this.plugins.install(name);\n\n    if (result) {\n      this.setState({\n        pluginBeingInstalled: undefined,\n        pluginsFromNpm: pluginsFromNpm.filter(p => p.name !== name),\n        pluginsInstalled: [result, ...pluginsInstalled].sort((a, b) => a.prettyName.localeCompare(b.prettyName))\n      });\n    } else {\n      this.setState({\n        pluginBeingInstalled: undefined\n      });\n    }\n  };\n\n  uninstall = async name => {\n    const {pluginsInstalled, pluginsFromNpm} = this.state;\n\n    const onTransitionEnd = async () => {\n      const plugin = await this.plugins.uninstall(name);\n      this.setState({\n        pluginsInstalled: pluginsInstalled.filter(p => p.name !== name),\n        pluginsFromNpm: [plugin, ...pluginsFromNpm].sort((a, b) => a.prettyName.localeCompare(b.prettyName)),\n        pluginBeingUninstalled: null,\n        onTransitionEnd: null\n      });\n    };\n\n    this.setState({pluginBeingUninstalled: name, onTransitionEnd});\n  };\n\n  openPluginsConfig = async name => {\n    this.track(`plugin/config/${name}`);\n    this.scrollIntoView('installed', name);\n    this.setState({category: 'plugins'});\n    this.setOverlay(true);\n    await this.plugins.openPluginConfig(name);\n    ipc.callMain('refresh-usage');\n    this.setOverlay(false);\n  };\n\n  openPluginsFolder = () => electron.shell.openPath(this.plugins.pluginsDir);\n\n  selectCategory = category => {\n    this.setState({category});\n  };\n\n  selectTab = tab => {\n    this.track(`preferences/tab/${tab}`);\n    this.setState({tab});\n  };\n\n  toggleSetting = (setting, value) => {\n    const newValue = value === undefined ? !this.state[setting] : value;\n    if (!SETTINGS_ANALYTICS_BLACKLIST.includes(setting)) {\n      this.track(`preferences/setting/${setting}/${newValue}`);\n    }\n\n    this.setState({[setting]: newValue});\n    this.settings.set(setting, newValue);\n  };\n\n  toggleRecordAudio = async () => {\n    const newValue = !this.state.recordAudio;\n    this.track(`preferences/setting/recordAudio/${newValue}`);\n\n    if (!newValue || await this.systemPermissions.ensureMicrophonePermissions()) {\n      if (newValue) {\n        try {\n          await this.getAudioDevices();\n        } catch (error) {\n          this.showError(error);\n        }\n      }\n\n      this.setState({recordAudio: newValue});\n      this.settings.set('recordAudio', newValue);\n    }\n  };\n\n  toggleShortcuts = async () => {\n    const setting = 'enableShortcuts';\n    const newValue = !this.state[setting];\n    this.toggleSetting(setting, newValue);\n    await ipc.callMain('toggle-shortcuts', {enabled: newValue});\n  };\n\n  updateShortcut = async (setting, shortcut) => {\n    try {\n      await ipc.callMain('update-shortcut', {setting, shortcut});\n      this.setState({\n        shortcuts: {\n          ...this.state.shortcuts,\n          [setting]: shortcut\n        }\n      });\n    } catch (error) {\n      console.warn('Error updating shortcut', error);\n    }\n  };\n\n  setOpenOnStartup = value => {\n    const openOnStartup = typeof value === 'boolean' ? value : !this.state.openOnStartup;\n    this.setState({openOnStartup});\n    this.remote.app.setLoginItemSettings({openAtLogin: openOnStartup});\n  };\n\n  pickKapturesDir = () => {\n    const {dialog, getCurrentWindow} = this.remote;\n\n    const directories = dialog.showOpenDialogSync(getCurrentWindow(), {\n      properties: [\n        'openDirectory',\n        'createDirectory'\n      ]\n    });\n\n    if (directories) {\n      this.toggleSetting('kapturesDir', directories[0]);\n    }\n  };\n\n  setAudioInputDeviceId = id => {\n    this.setState({audioInputDeviceId: id});\n    this.settings.set('audioInputDeviceId', id);\n  };\n}\n"
  },
  {
    "path": "renderer/hooks/dark-mode.tsx",
    "content": "import {useState, useEffect} from 'react';\n\nconst useDarkMode = () => {\n  const {darkMode} = require('electron-util');\n  const [isDarkMode, setIsDarkMode] = useState(darkMode.isEnabled);\n\n  useEffect(() => {\n    return darkMode.onChange(() => {\n      setIsDarkMode(darkMode.isEnabled);\n    });\n  }, []);\n\n  return isDarkMode;\n};\n\nexport default useDarkMode;\n"
  },
  {
    "path": "renderer/hooks/editor/use-conversion-id.tsx",
    "content": "import {CreateExportOptions} from 'common/types';\nimport {ipcRenderer} from 'electron-better-ipc';\nimport {createContext, PropsWithChildren, useContext, useMemo, useState} from 'react';\n\nconst ConversionIdContext = createContext<{\n  conversionId: string;\n  setConversionId: (id: string) => void;\n  startConversion: (options: CreateExportOptions) => Promise<void>;\n}>(undefined);\n\nlet savedConversionId: string;\n\nexport const ConversionIdContextProvider = (props: PropsWithChildren<Record<string, unknown>>) => {\n  const [conversionId, setConversionId] = useState<string>();\n\n  const startConversion = async (options: CreateExportOptions) => {\n    const id = await ipcRenderer.callMain<CreateExportOptions, string>('create-export', options);\n    setConversionId(id);\n  };\n\n  const updateConversionId = (id: string) => {\n    savedConversionId = savedConversionId || id;\n    setConversionId(id || savedConversionId);\n  };\n\n  const value = useMemo(() => ({\n    conversionId,\n    setConversionId: updateConversionId,\n    startConversion\n  }), [conversionId, setConversionId]);\n\n  return (\n    <ConversionIdContext.Provider value={value}>\n      {props.children}\n    </ConversionIdContext.Provider>\n  );\n};\n\nconst useConversionIdContext = () => useContext(ConversionIdContext);\n\nexport default useConversionIdContext;\n"
  },
  {
    "path": "renderer/hooks/editor/use-conversion.tsx",
    "content": "import {ExportsRemoteState} from 'common/types';\nimport createRemoteStateHook from 'hooks/use-remote-state';\n\nconst useConversion = createRemoteStateHook<ExportsRemoteState>('exports');\n\nexport type UseConversion = ReturnType<typeof useConversion>;\nexport type UseConversionState = UseConversion['state'];\nexport default useConversion;\n"
  },
  {
    "path": "renderer/hooks/editor/use-editor-options.tsx",
    "content": "import {EditorOptionsRemoteState} from 'common/types';\nimport createRemoteStateHook from 'hooks/use-remote-state';\n\nconst useEditorOptions = createRemoteStateHook<EditorOptionsRemoteState>('editor-options', {\n  formats: [],\n  editServices: [],\n  fpsHistory: {\n    gif: 60,\n    mp4: 60,\n    av1: 60,\n    webm: 60,\n    apng: 60,\n    hevc: 60\n  }\n});\n\nexport type EditorOptionsState = ReturnType<typeof useEditorOptions>['state'];\nexport default useEditorOptions;\n"
  },
  {
    "path": "renderer/hooks/editor/use-editor-window-state.tsx",
    "content": "import useWindowState from 'hooks/window-state';\nimport {EditorWindowState} from 'common/types';\n\nconst useEditorWindowState = () => useWindowState<EditorWindowState>();\nexport default useEditorWindowState;\n"
  },
  {
    "path": "renderer/hooks/editor/use-share-plugins.tsx",
    "content": "import OptionsContainer from 'components/editor/options-container';\nimport {remote} from 'electron';\nimport {ipcRenderer} from 'electron-better-ipc';\nimport {useMemo} from 'react';\n\nconst useSharePlugins = () => {\n  const {\n    formats,\n    format,\n    sharePlugin,\n    updateSharePlugin\n  } = OptionsContainer.useContainer();\n\n  const menuOptions = useMemo(() => {\n    const selectedFormat = formats.find(f => f.format === format);\n\n    let onlyBuiltIn = true;\n    const options = selectedFormat?.plugins?.map(plugin => {\n      if (plugin.apps && plugin.apps.length > 0) {\n        const subMenu = plugin.apps.map(app => ({\n          label: app.isDefault ? `${app.name} (default)` : app.name,\n          type: 'radio',\n          checked: sharePlugin.app?.url === app.url,\n          value: {\n            pluginName: plugin.pluginName,\n            serviceTitle: plugin.title,\n            app\n          },\n          icon: remote.nativeImage.createFromDataURL(app.icon).resize({width: 16, height: 16})\n        }));\n\n        if (plugin.apps[0].isDefault) {\n          subMenu.splice(1, 0, {type: 'separator'} as any);\n        }\n\n        return {\n          isBuiltIn: true,\n          subMenu,\n          value: {\n            pluginName: plugin.pluginName,\n            serviceTitle: plugin.title,\n            app: plugin.apps[0]\n          },\n          checked: sharePlugin.pluginName === plugin.pluginName,\n          label: 'Open With…'\n        };\n      }\n\n      if (!plugin.pluginName.startsWith('_')) {\n        onlyBuiltIn = false;\n      }\n\n      return {\n        value: {\n          pluginName: plugin.pluginName,\n          serviceTitle: plugin.title\n        },\n        checked: sharePlugin.pluginName === plugin.pluginName,\n        label: plugin.title\n      };\n    });\n\n    if (onlyBuiltIn) {\n      options?.push({\n        separator: true\n      } as any, {\n        label: 'Get Plugins…',\n        checked: false,\n        click: () => {\n          ipcRenderer.callMain('open-preferences', {category: 'plugins', tab: 'discover'});\n        }\n      } as any);\n    }\n\n    return options ?? [];\n  }, [formats, format, sharePlugin]);\n\n  const label = sharePlugin?.app ? sharePlugin.app.name : sharePlugin?.serviceTitle;\n\n  return {menuOptions, label, onChange: updateSharePlugin};\n};\n\nexport default useSharePlugins;\n"
  },
  {
    "path": "renderer/hooks/editor/use-window-size.tsx",
    "content": "import {remote} from 'electron';\nimport {useEffect, useRef} from 'react';\nimport {resizeKeepingCenter} from 'utils/window';\n\nconst CONVERSION_WIDTH = 370;\nconst CONVERSION_HEIGHT = 392;\nconst DEFAULT_EDITOR_WIDTH = 768;\nconst DEFAULT_EDITOR_HEIGHT = 480;\n\nexport const useEditorWindowSizeEffect = (isConversionWindowState: boolean) => {\n  const previousWindowSizeRef = useRef<{width: number; height: number}>();\n\n  useEffect(() => {\n    if (!previousWindowSizeRef.current) {\n      previousWindowSizeRef.current = {\n        width: DEFAULT_EDITOR_WIDTH,\n        height: DEFAULT_EDITOR_HEIGHT\n      };\n      return;\n    }\n\n    const window = remote.getCurrentWindow();\n    const bounds = window.getBounds();\n\n    if (isConversionWindowState) {\n      previousWindowSizeRef.current = {\n        width: bounds.width,\n        height: bounds.height\n      };\n\n      window.setBounds(resizeKeepingCenter(bounds, {width: CONVERSION_WIDTH, height: CONVERSION_HEIGHT}), true);\n      window.resizable = false;\n      window.fullScreenable = false;\n    } else {\n      window.resizable = true;\n      window.fullScreenable = true;\n      window.setBounds(resizeKeepingCenter(bounds, previousWindowSizeRef.current), true);\n    }\n  }, [isConversionWindowState]);\n};\n"
  },
  {
    "path": "renderer/hooks/exports/use-exports-list.tsx",
    "content": "import {ExportsListRemoteState} from 'common/types';\nimport createRemoteStateHook from 'hooks/use-remote-state';\n\nconst useExportsList = createRemoteStateHook<ExportsListRemoteState>('exports-list');\n\nexport type UseExportsList = ReturnType<typeof useExportsList>;\nexport type UseExportsListState = UseExportsList['state'];\nexport default useExportsList;\n"
  },
  {
    "path": "renderer/hooks/use-confirmation.tsx",
    "content": "import {useCallback} from 'react';\n\ninterface UseConfirmationOptions {\n  message: string;\n  detail?: string;\n  confirmButtonText: string;\n  cancelButtonText?: string;\n}\n\nexport const useConfirmation = (\n  callback: () => void,\n  options: UseConfirmationOptions\n) => {\n  return useCallback(() => {\n    const {dialog, remote} = require('electron-util').api;\n\n    const buttonIndex = dialog.showMessageBoxSync(remote.getCurrentWindow(), {\n      type: 'question',\n      buttons: [\n        options.confirmButtonText,\n        options.cancelButtonText ?? 'Cancel'\n      ],\n      defaultId: 0,\n      cancelId: 1,\n      message: options.message,\n      detail: options.detail\n    });\n\n    if (buttonIndex === 0) {\n      callback();\n    }\n  }, [callback]);\n};\n"
  },
  {
    "path": "renderer/hooks/use-current-window.tsx",
    "content": "import {remote} from 'electron';\n\nexport const useCurrentWindow = () => {\n  return remote.getCurrentWindow();\n};\n"
  },
  {
    "path": "renderer/hooks/use-keyboard-action.tsx",
    "content": "import {DependencyList, useEffect, useMemo} from 'react';\n\nexport const useKeyboardAction = (keyOrFilter: string | ((key: string, eventType: string) => boolean), action: () => void, deps: DependencyList = []) => {\n  const isArgFilter = typeof keyOrFilter === 'function';\n  const filter = useMemo(() => typeof keyOrFilter === 'function' ? keyOrFilter : (key: string) => key === keyOrFilter, [keyOrFilter]);\n\n  useEffect(() => {\n    const handler = (event: KeyboardEvent) => {\n      if (filter(event.key, event.type)) {\n        action();\n      }\n    };\n\n    document.addEventListener('keyup', handler);\n    if (isArgFilter) {\n      document.addEventListener('keydown', handler);\n      document.addEventListener('keypress', handler);\n    }\n\n    return () => {\n      document.removeEventListener('keyup', handler);\n\n      if (isArgFilter) {\n        document.removeEventListener('keypress', handler);\n        document.removeEventListener('keydown', handler);\n      }\n    };\n  }, [...deps, filter, action]);\n};\n"
  },
  {
    "path": "renderer/hooks/use-remote-state.tsx",
    "content": "import {useState, useEffect, useRef} from 'react';\nimport {ipcRenderer} from 'electron-better-ipc';\nimport {RemoteState, RemoteStateHook} from '../common/types';\n\n// TODO: Import these util exports from the `main/remote-states/utils` file once we figure out the correct TS configuration\nexport const getChannelName = (name: string, action: string) => `kap-remote-state-${name}-${action}`;\n\nexport const getChannelNames = (name: string) => ({\n  subscribe: getChannelName(name, 'subscribe'),\n  getState: getChannelName(name, 'get-state'),\n  callAction: getChannelName(name, 'call-action'),\n  stateUpdated: getChannelName(name, 'state-updated')\n});\n\nconst createRemoteStateHook = <Callback extends RemoteState>(\n  name: string,\n  initialState?: Callback extends RemoteState<infer State> ? State : never\n): (id?: string) => RemoteStateHook<Callback> => {\n  const channelNames = getChannelNames(name);\n\n  return (id?: string) => {\n    const [state, setState] = useState(initialState);\n    const [isLoading, setIsLoading] = useState(true);\n    const actionsRef = useRef<any>({});\n\n    useEffect(() => {\n      const cleanup = ipcRenderer.answerMain(channelNames.stateUpdated, (data: {id?: string; state: any}) => {\n        if (data.id === id) {\n          setState(data.state);\n        }\n      });\n\n      (async () => {\n        const actionKeys = (await ipcRenderer.callMain<string, string[]>(channelNames.subscribe, id));\n\n        // eslint-disable-next-line unicorn/no-array-reduce\n        const actions = actionKeys.reduce((acc, key) => ({\n          ...acc,\n          [key]: async (...data: any) => ipcRenderer.callMain(channelNames.callAction, {key, data, id})\n        }), {});\n\n        const getState = async () => {\n          const newState = (await ipcRenderer.callMain<string, any>(channelNames.getState, id));\n          setState(newState);\n        };\n\n        actionsRef.current = {\n          ...actions,\n          refreshState: getState\n        };\n\n        await getState();\n        setIsLoading(false);\n      })();\n\n      return cleanup;\n    }, []);\n\n    return {\n      ...actionsRef.current,\n      isLoading,\n      state\n    };\n  };\n};\n\nexport default createRemoteStateHook;\n"
  },
  {
    "path": "renderer/hooks/use-show-window.tsx",
    "content": "import {useEffect} from 'react';\nimport {ipcRenderer} from 'electron-better-ipc';\n\nexport const useShowWindow = (show: boolean) => {\n  useEffect(() => {\n    if (show) {\n      ipcRenderer.callMain('kap-window-mount');\n    }\n  }, [show]);\n};\n"
  },
  {
    "path": "renderer/hooks/window-state.tsx",
    "content": "import {createContext, useContext, useState, useEffect, ReactNode} from 'react';\nimport {ipcRenderer as ipc} from 'electron-better-ipc';\n\nconst WindowStateContext = createContext<any>(undefined);\n\nexport const WindowStateProvider = (props: {children: ReactNode}) => {\n  const [windowState, setWindowState] = useState();\n\n  useEffect(() => {\n    ipc.callMain('kap-window-state').then(setWindowState);\n\n    return ipc.answerMain('kap-window-state', (newState: any) => {\n      setWindowState(newState);\n    });\n  }, []);\n\n  return (\n    <WindowStateContext.Provider value={windowState}>\n      {props.children}\n    </WindowStateContext.Provider>\n  );\n};\n\n// Should not be used directly\n// Each page should export its own typed hook\n// eslint-disable-next-line @typescript-eslint/comma-dangle\nconst useWindowState = <T,>() => useContext<T>(WindowStateContext);\n\nexport default useWindowState;\n"
  },
  {
    "path": "renderer/next-env.d.ts",
    "content": "/// <reference types=\"next\" />\n/// <reference types=\"next/types/global\" />\n"
  },
  {
    "path": "renderer/next.config.js",
    "content": "const path = require('path');\n\nmodule.exports = (nextConfig) => {\n  return Object.assign({}, nextConfig, {\n    webpack(config, options) {\n      config.module.rules.push({\n        test: /\\.+(js|jsx|mjs|ts|tsx)$/,\n        loader: options.defaultLoaders.babel,\n        include: [\n          path.join(__dirname, '..', 'main', 'common'),\n          path.join(__dirname, '..', 'main', 'remote-states', 'use-remote-state.ts')\n        ]\n      });\n\n      config.target = 'electron-renderer';\n      config.devtool = 'cheap-module-source-map';\n\n      if (typeof nextConfig.webpack === 'function') {\n        return nextConfig.webpack(config, options);\n      }\n\n      return config;\n    }\n  })\n}\n"
  },
  {
    "path": "renderer/pages/_app.tsx",
    "content": "import {AppProps} from 'next/app';\nimport {useState, useEffect} from 'react';\nimport useDarkMode from '../hooks/dark-mode';\nimport GlobalStyles from '../utils/global-styles';\nimport SentryErrorBoundary from '../utils/sentry-error-boundary';\nimport {WindowStateProvider} from '../hooks/window-state';\nimport classNames from 'classnames';\n\nconst Kap = (props: AppProps) => {\n  const [isMounted, setIsMounted] = useState(false);\n\n  useEffect(() => {\n    setIsMounted(true);\n  }, []);\n\n  if (!isMounted) {\n    return null;\n  }\n\n  return <MainApp {...props}/>;\n};\n\nconst MainApp = ({Component, pageProps}: AppProps) => {\n  const isDarkMode = useDarkMode();\n  const className = classNames('cover-window', {dark: isDarkMode});\n\n  return (\n    <div className={className}>\n      <SentryErrorBoundary>\n        <WindowStateProvider>\n          <Component {...pageProps}/>\n          <GlobalStyles/>\n        </WindowStateProvider>\n      </SentryErrorBoundary>\n    </div>\n  );\n};\n\nexport default Kap;\n"
  },
  {
    "path": "renderer/pages/config.js",
    "content": "import React from 'react';\nimport {Provider} from 'unstated';\nimport {ipcRenderer as ipc} from 'electron-better-ipc';\n\nimport {ConfigContainer} from '../containers';\nimport Config from '../components/config';\nimport WindowHeader from '../components/window-header';\n\nconst configContainer = new ConfigContainer();\n\nexport default class ConfigPage extends React.Component {\n  state = {title: ''};\n\n  componentDidMount() {\n    ipc.answerMain('plugin', pluginName => {\n      configContainer.setPlugin(pluginName);\n      this.setState({title: pluginName.replace(/^kap-/, '')});\n    });\n\n    ipc.answerMain('edit-service', ({pluginName, serviceTitle}) => {\n      configContainer.setEditService(pluginName, serviceTitle);\n      this.setState({title: serviceTitle});\n    });\n  }\n\n  render() {\n    const {title} = this.state;\n\n    return (\n      <div className=\"root\">\n        <div className=\"cover-window\">\n          <Provider inject={[configContainer]}>\n            <WindowHeader title={title}/>\n            <Config/>\n          </Provider>\n        </div>\n        <style jsx global>{`\n          html {\n            font-size: 62.5%;\n          }\n\n          html,\n          body,\n          .cover-window {\n            margin: 0;\n            width: 100vw;\n            height: 100vh;\n            font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', sans-serif;\n          }\n\n          :root {\n            --background-color: #ffffff;\n            --button-color: var(--kap);\n          }\n\n          .dark .cover-window {\n            --background-color: #313234;\n            --button-color: #2182f0;\n          }\n\n          .cover-window {\n            background-color: var(--background-color);\n            z-index: -2;\n            display: flex;\n            flex-direction: column;\n            font-size: 1.4rem;\n            line-height: 1.5em;\n            -webkit-font-smoothing: antialiased;\n            letter-spacing: -.01rem;\n            cursor: default;\n          }\n        `}</style>\n      </div>\n    );\n  }\n}\n"
  },
  {
    "path": "renderer/pages/cropper.js",
    "content": "import electron from 'electron';\nimport React from 'react';\nimport {Provider} from 'unstated';\n\nimport Overlay from '../components/cropper/overlay';\nimport Cropper from '../components/cropper';\nimport ActionBar from '../components/action-bar';\n\nimport CursorContainer from '../containers/cursor';\nimport CropperContainer from '../containers/cropper';\nimport ActionBarContainer from '../containers/action-bar';\n\nconst cursorContainer = new CursorContainer();\nconst cropperContainer = new CropperContainer();\nconst actionBarContainer = new ActionBarContainer();\n\ncropperContainer.bindCursor(cursorContainer);\ncropperContainer.bindActionBar(actionBarContainer);\nactionBarContainer.bindCursor(cursorContainer);\nactionBarContainer.bindCropper(cropperContainer);\n\nlet lastRatioLockState = null;\n\nexport default class CropperPage extends React.Component {\n  remote = electron.remote || false;\n\n  dev = false;\n\n  constructor(props) {\n    super(props);\n\n    if (!electron.ipcRenderer) {\n      return;\n    }\n\n    const {ipcRenderer, remote} = electron;\n\n    ipcRenderer.on('display', (_, display) => {\n      cropperContainer.setDisplay(display);\n      actionBarContainer.setDisplay(display);\n    });\n\n    ipcRenderer.on('select-app', (_, app) => {\n      cropperContainer.selectApp(app);\n      cropperContainer.setActive(true);\n    });\n\n    ipcRenderer.on('blur', () => {\n      cropperContainer.setActive(false);\n    });\n\n    ipcRenderer.on('start-recording', () => {\n      cropperContainer.setRecording();\n    });\n\n    const window = remote.getCurrentWindow();\n    window.on('focus', () => {\n      cropperContainer.setActive(true);\n    });\n\n    window.on('blur', event => {\n      if (!event.defaultPrevented) {\n        cropperContainer.setActive(false);\n      }\n    });\n  }\n\n  componentDidMount() {\n    document.addEventListener('keydown', this.handleKeyEvent);\n    document.addEventListener('keyup', this.handleKeyEvent);\n  }\n\n  componentWillUnmount() {\n    document.removeEventListener('keydown', this.handleKeyEvent);\n    document.removeEventListener('keyup', this.handleKeyEvent);\n  }\n\n  handleKeyEvent = event => {\n    switch (event.key) {\n      case 'Escape':\n        this.remote.getCurrentWindow().close();\n        break;\n      case 'Shift':\n        if (event.type === 'keydown' && !event.defaultPrevented) {\n          lastRatioLockState = actionBarContainer.state.ratioLocked;\n          actionBarContainer.toggleRatioLock(true);\n        } else if (event.type === 'keyup' && lastRatioLockState !== null) {\n          actionBarContainer.toggleRatioLock(lastRatioLockState);\n          lastRatioLockState = null;\n        }\n\n        break;\n      case 'Alt':\n        cropperContainer.toggleResizeFromCenter(event.type === 'keydown');\n        break;\n      case 'i':\n        this.remote.getCurrentWindow().setIgnoreMouseEvents(true);\n        this.dev = !this.dev;\n        break;\n      default:\n        break;\n    }\n  };\n\n  render() {\n    return (\n      <div className=\"cover-screen\">\n        <Provider inject={[cursorContainer, cropperContainer, actionBarContainer]}>\n          <Overlay>\n            <Cropper/>\n            <ActionBar/>\n          </Overlay>\n        </Provider>\n        <style jsx global>{`\n          html,\n          body,\n          .cover-screen {\n            margin: 0;\n            width: 100vw;\n            height: 100vh;\n            user-select: none;\n            display: flex;\n            font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', sans-serif;\n          }\n\n          .content {\n            flex: 1;\n            display: flex;\n          }\n\n          @keyframes shake {\n            10%,\n            90% {\n              transform: translate3d(-1px, 0, 0);\n            }\n\n            20%,\n            80% {\n              transform: translate3d(2px, 0, 0);\n            }\n\n            30%,\n            50%,\n            70% {\n              transform: translate3d(-4px, 0, 0);\n            }\n\n            40%,\n            60% {\n              transform: translate3d(4px, 0, 0);\n            }\n          }\n\n          .shake {\n            transform: translate3d(0, 0, 0);\n            backface-visibility: hidden;\n            perspective: 1000px;\n            animation: shake 0.82s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;\n          }\n\n          :root {\n            --action-bar-box-shadow: 0 20px 40px 0 rgba(0, 0, 0, .2);\n            --action-bar-background: #ffffff;\n            --action-bar-border: none;\n\n            --record-button-border-color: var(--red);\n            --record-button-background: #ff6059 radial-gradient(ellipse 100% 0% at 50% 0%, #ff6159 0%, #ff5f52 50%, #ff3a30 100%);\n            --record-button-focus-background: #ff6059 radial-gradient(ellipse 100% 0% at 50% 0%, #ff6159 0%, #ff5f52 50%, #ff3a30 100%);\n            --record-button-focus-background-cropper: var(--record-button-focus-background);\n            --record-button-focus-outter-background: #ffffff;\n            --record-button-focus-outter-border: var(--record-button-border-color);\n            --record-button-ripple-color: var(--red);\n            --record-button-fill-background: var(--record-button-background);\n            --record-button-inner-background: #fff;\n            --record-button-inner-background-cropper: #fff;\n            --record-button-inner-border-width: 0px;\n\n            --input-hover-border-color: #ccc;\n            --input-border-color: #dbdbdb;\n            --button-active-color: #f7f7f7;\n            --record-button-inner-border: transparent;\n          }\n\n          .dark {\n            --action-bar-box-shadow: 0 20px 40px 0 rgba(0, 0, 0, 0.4);\n            --action-bar-background: #222222;\n            --action-bar-border: 1px solid rgba(0, 0, 0, 0.8);\n\n            --icon-color: rgba(255, 255, 255, 0.6);\n\n            --input-hover-border-color: transparent;\n            --button-active-color: var(--input-background-color);\n            --record-button-focus-outter-background: #222222;\n          }\n        `}</style>\n      </div>\n    );\n  }\n}\n"
  },
  {
    "path": "renderer/pages/dialog.js",
    "content": "import Actions from '../components/dialog/actions';\nimport Icon from '../components/dialog/icon';\nimport Body from '../components/dialog/body';\nimport React, {useState, useEffect, useRef, useCallback} from 'react';\nimport {ipcRenderer as ipc} from 'electron-better-ipc';\n\nlet measureResolve;\n\nconst Dialog = () => {\n  const [data, setData] = useState();\n  const [measureSize, setMeasureSize] = useState(false);\n  const [isDisabled, setIsDisabled] = useState(false);\n  const container = useRef(null);\n\n  const performAction = useCallback(async index => {\n    if (!isDisabled || data.cancelId === index) {\n      setIsDisabled(true);\n      return ipc.callMain(`dialog-action-${data.id}`, index);\n    }\n  }, [data, isDisabled, setIsDisabled]);\n\n  useEffect(() => {\n    return ipc.answerMain('data', async newData => new Promise(resolve => {\n      setData(newData);\n      setMeasureSize(true);\n      measureResolve = resolve;\n    }));\n  }, []);\n\n  useEffect(() => {\n    if (data) {\n      setIsDisabled(false);\n    }\n\n    if (data && data.cancelId) {\n      const handler = event => {\n        if (event.code === 'Escape') {\n          performAction(data.cancelId);\n        }\n      };\n\n      document.addEventListener('keydown', handler);\n\n      return () => {\n        document.removeEventListener('keydown', handler);\n      };\n    }\n  }, [data, performAction, isDisabled, setIsDisabled]);\n\n  useEffect(() => {\n    if (measureSize && measureResolve) {\n      const {offsetWidth, offsetHeight} = container.current;\n      measureResolve({width: offsetWidth, height: offsetHeight});\n      measureResolve = undefined;\n      setMeasureSize(false);\n    }\n  }, [measureSize]);\n\n  if (!data) {\n    return null;\n  }\n\n  return (\n    <div ref={container} className=\"dialog-container\">\n      <Icon/>\n      <div className=\"dialog-content\">\n        <Body title={data.title} message={data.message} detail={data.detail}/>\n        <Actions buttons={data.buttons} performAction={performAction} defaultId={data.defaultId}/>\n      </div>\n      <style jsx global>{`\n        html {\n          font-size: 62.5%;\n        }\n\n        html,\n        body {\n          margin: 0;\n          width: 100vw;\n          height: 100vh;\n          font-family: system-ui, 'Helvetica Neue', sans-serif;\n          background: transparent;\n        }\n\n        .dialog-container {\n          display: flex;\n          width: ${measureSize ? 'max-content' : '100%'};\n          height: ${measureSize ? 'max-content' : '100%'};\n        }\n\n        .dialog-content {\n          display: flex;\n          flex-direction: column;\n          width: min-content;\n          flex: 1;\n        }\n\n        button {\n          border: 1px solid rgba(0, 0, 0, 0.2);\n          box-shadow: inset 0px 1px 0px 0px rgba(255, 255, 255, 0.04), 0px 1px 0px 0px rgba(0, 0, 0, 0.05);\n          outline: none;\n        }\n\n        button:active,\n        button:focus {\n          border-color: var(--kap);\n          outline: none;\n        }\n\n        .dark button {\n          background: rgba(255, 255, 255, 0.1);\n          border: none;\n        }\n\n        .dark button:active,\n        .dark button:focus {\n          background: hsla(0, 0%, 100%, 0.2);\n        }\n      `}</style>\n    </div>\n  );\n};\n\nexport default Dialog;\n"
  },
  {
    "path": "renderer/pages/editor.tsx",
    "content": "import Head from 'next/head';\n// Import EditorPreview from '../components/editor/editor-preview';\nimport combineUnstatedContainers from '../utils/combine-unstated-containers';\nimport VideoMetadataContainer from '../components/editor/video-metadata-container';\nimport VideoTimeContainer from '../components/editor/video-time-container';\nimport VideoControlsContainer from '../components/editor/video-controls-container';\nimport OptionsContainer from '../components/editor/options-container';\nimport useEditorWindowState from 'hooks/editor/use-editor-window-state';\nimport {ConversionIdContextProvider} from 'hooks/editor/use-conversion-id';\nimport Editor from 'components/editor';\n\nconst ContainerProvider = combineUnstatedContainers([\n  OptionsContainer,\n  VideoMetadataContainer,\n  VideoTimeContainer,\n  VideoControlsContainer\n]) as any;\n\nconst EditorPage = () => {\n  const args = useEditorWindowState();\n\n  if (!args) {\n    return null;\n  }\n\n  return (\n    <div className=\"cover-window\">\n      <Head>\n        <meta httpEquiv=\"Content-Security-Policy\" content=\"media-src file:;\"/>\n      </Head>\n      <ConversionIdContextProvider>\n        <ContainerProvider>\n          <Editor/>\n        </ContainerProvider>\n      </ConversionIdContextProvider>\n      <style jsx global>{`\n        :root {\n          --slider-popup-background: rgba(255, 255, 255, 0.85);\n          --slider-background-color: #ffffff;\n          --slider-thumb-color: #ffffff;\n          --background-color: #222222;\n        }\n\n        .dark {\n          --slider-popup-background: #222222;\n          --slider-background-color: var(--input-background-color);\n          --slider-thumb-color: var(--storm);\n        }\n\n        .preview-hover-container:hover .video-controls {\n          bottom: 0;\n        }\n\n        .preview-hover-container:not(:hover) .progress-bar-container {\n          bottom: 64px;\n          width: 100%\n        }\n\n        .preview-hover-container:not(:hover) .progress-bar-container .progress-bar {\n          border-radius: 0;\n        }\n\n        .preview-hover-container:not(:hover) .progress-bar-container .slider {\n          display: none;\n        }\n\n        .cover-window {\n          -webkit-app-region: drag;\n          user-select: none;\n          background-color: #222222;\n        }\n\n        .tooltip {\n          padding: 0 !important;\n          max-width: 300px;\n        }\n\n        .tooltip-content {\n          padding: 8px 21px;\n        }\n\n        .hide-tooltip .tooltip {\n          display: none;\n        }\n      `}</style>\n    </div>\n  );\n};\n\nexport default EditorPage;\n"
  },
  {
    "path": "renderer/pages/exports.tsx",
    "content": "import React from 'react';\n\nimport WindowHeader from '../components/window-header';\nimport Exports from '../components/exports';\n\nconst ExportsPage = () => (\n  <div className=\"cover-window\">\n    <WindowHeader title=\"Exports\"/>\n    <Exports/>\n    <style jsx global>{`\n        :root {\n          --thumbnail-overlay-color: rgba(0, 0, 0, 0.4);\n          --row-hover-color: #f9f9f9;\n          --background-color: #fff;\n        }\n\n        .dark {\n          --thumbnail-overlay-color: rgba(0, 0, 0, 0.2);\n          --row-hover-color: rgba(255, 255, 255, 0.1);\n          --background-color: #222222;\n        }\n    `}</style>\n  </div>\n);\n\nexport default ExportsPage;\n"
  },
  {
    "path": "renderer/pages/preferences.js",
    "content": "import React from 'react';\nimport {Provider} from 'unstated';\nimport classNames from 'classnames';\nimport {ipcRenderer as ipc} from 'electron-better-ipc';\n\nimport PreferencesNavigation from '../components/preferences/navigation';\nimport WindowHeader from '../components/window-header';\nimport Categories from '../components/preferences/categories';\n\nimport PreferencesContainer from '../containers/preferences';\n\nconst preferencesContainer = new PreferencesContainer();\n\nexport default class PreferencesPage extends React.Component {\n  state = {overlay: false};\n\n  componentDidMount() {\n    ipc.answerMain('open-plugin-config', preferencesContainer.openPluginsConfig);\n    ipc.answerMain('options', preferencesContainer.setNavigation);\n    ipc.answerMain('mount', async () => preferencesContainer.mount(this.setOverlay));\n  }\n\n  setOverlay = overlay => {\n    this.setState({overlay});\n  };\n\n  render() {\n    const {overlay} = this.state;\n    const className = classNames('overlay', {active: overlay});\n\n    return (\n      <div className=\"cover-window\">\n        <div className={className}/>\n        <Provider inject={[preferencesContainer]}>\n          <WindowHeader title=\"Preferences\">\n            <PreferencesNavigation/>\n          </WindowHeader>\n          <Categories/>\n        </Provider>\n        <style jsx global>{`\n            html {\n              font-size: 62.5%;\n            }\n\n            html,\n            body,\n            .cover-window {\n              margin: 0;\n              width: 100vw;\n              height: 100vh;\n              font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', sans-serif;\n            }\n\n            .cover-window {\n              background-color: var(--window-background-color);\n              display: flex;\n              flex-direction: column;\n              font-size: 1.4rem;\n              line-height: 1.5em;\n              -webkit-font-smoothing: antialiased;\n              letter-spacing: -.01rem;\n              cursor: default;\n            }\n\n            .overlay {\n              position: fixed;\n              z-index: 12;\n              top: 0;\n              left: 0;\n              width: 100%;\n              height: 100%;\n              transition: background-color .3s ease-in-out;\n              pointer-events: none;\n            }\n\n            .overlay.active {\n              background-color: rgba(0,0,0,0.2);\n            }\n\n\n            @keyframes shake {\n              10%,\n              90% {\n                transform: translate3d(-1px, 0, 0);\n              }\n\n              20%,\n              80% {\n                transform: translate3d(2px, 0, 0);\n              }\n\n              30%,\n              50%,\n              70% {\n                transform: translate3d(-4px, 0, 0);\n              }\n\n              40%,\n              60% {\n                transform: translate3d(4px, 0, 0);\n              }\n            }\n\n            .shake {\n              transform: translate3d(0, 0, 0);\n              backface-visibility: hidden;\n              perspective: 1000px;\n              animation: shake 0.82s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;\n            }\n\n            :root {\n              --navigation-item-border-color: rgba(0, 0, 0, 0.1);\n              --navigation-item-box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.03);\n              --navigation-item-box-shadow-active: var(--navigation-item-box-shadow);\n              --navigation-item-background: transparent;\n              --navigation-item-outline-active: none;\n              --navigation-item-active-border-color: rgba(0, 0, 0, 0.2);\n              --navigation-item-hover-color: rgba(0, 0, 0, 0.8);\n              --background-color: #fff;\n            }\n\n            .dark {\n              --navigation-item-border-color: transparent;\n              --navigation-item-background: var(--input-background-color);\n              --navigation-item-box-shoadow-active: 0px 1px 2px 0px rgba(0, 0, 0, 0.03);\n              --navigation-item-outline-active: 1px solid rgba(0, 0, 0, 0.1);\n              --navigation-item-active-border-color: transparent;\n              --navigation-item-hover-color: rgba(255, 255, 255, .8);\n              --background-color: #222222;\n            }\n\n        `}</style>\n      </div>\n    );\n  }\n}\n"
  },
  {
    "path": "renderer/tsconfig.eslint.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"include\": [\n    \"next-env.d.ts\",\n    \"**/*.ts\",\n    \"**/*.tsx\",\n    \"**/*.js\"\n  ]\n}\n"
  },
  {
    "path": "renderer/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"es2019\",\n    \"lib\": [\n      \"dom\",\n      \"dom.iterable\",\n      \"esnext\"\n    ],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": false,\n    \"forceConsistentCasingInFileNames\": true,\n    \"noEmit\": true,\n    \"preserveSymlinks\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"node\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"preserve\",\n    \"downlevelIteration\": true,\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"utils/*\": [\"./utils/*\"],\n      \"components/*\": [\"./components/*\"],\n      \"containers/*\": [\"./containers/*\"],\n      \"hooks/*\": [\"./hooks/*\"],\n      \"common/*\": [\"./common/*\"],\n      \"vectors\": [\"./vectors\"]\n    }\n  },\n  \"exclude\": [\n    \"node_modules\",\n    \"common-remote-states\"\n  ],\n  \"include\": [\n    \"next-env.d.ts\",\n    \"**/*.ts\",\n    \"**/*.tsx\"\n  ]\n}\n"
  },
  {
    "path": "renderer/utils/combine-unstated-containers.tsx",
    "content": "import React, {FunctionComponent, PropsWithChildren} from 'react';\nimport {Container} from 'unstated-next';\n\ntype ContainerOrWithInitialState<T = any> = Container<any, T> | [Container<any, T>, T];\n\nconst combineUnstatedContainers = (containers: ContainerOrWithInitialState[]) => ({children}: PropsWithChildren<Record<string, unknown>>) => {\n  // eslint-disable-next-line unicorn/no-array-reduce\n  return containers.reduce<React.ReactElement>(\n    (tree, ContainerOrWithInitialState) => {\n      if (Array.isArray(ContainerOrWithInitialState)) {\n        const [Container, initialState] = ContainerOrWithInitialState;\n        return <Container.Provider initialState={initialState}>{tree}</Container.Provider>;\n      }\n\n      return <ContainerOrWithInitialState.Provider>{tree}</ContainerOrWithInitialState.Provider>;\n    },\n    // @ts-expect-error\n    children\n  );\n};\n\nexport default combineUnstatedContainers;\n"
  },
  {
    "path": "renderer/utils/format-time.js",
    "content": "import moment from 'moment';\n\nconst formatTime = (time, options) => {\n  options = {\n    showMilliseconds: false,\n    ...options\n  };\n\n  const durationFormatted = options.extra ?\n    `  (${format(options.extra, options)})` :\n    '';\n\n  return `${format(time, options)}${durationFormatted}`;\n};\n\nconst format = (time, {showMilliseconds} = {}) => {\n  const formatString = `${time >= 60 * 60 ? 'hh:m' : ''}m:ss${showMilliseconds ? '.SS' : ''}`;\n\n  return moment().startOf('day').millisecond(time * 1000).format(formatString);\n};\n\nexport default formatTime;\n"
  },
  {
    "path": "renderer/utils/global-styles.tsx",
    "content": "import {useState, useEffect, useMemo} from 'react';\nimport useDarkMode from '../hooks/dark-mode';\nimport {remote} from 'electron';\n\nconst GlobalStyles = () => {\n  const [accentColor, setAccentColor] = useState(remote.systemPreferences.getAccentColor());\n  const isDarkMode = useDarkMode();\n\n  const systemColors = useMemo(() => {\n    return systemColorNames\n      .map(name => `--system-${name}: ${remote.systemPreferences.getColor(name as any)};`)\n      .join('\\n');\n  }, [isDarkMode]);\n\n  const updateAccentColor = (_, accentColor) => {\n    setAccentColor(accentColor);\n  };\n\n  useEffect(() => {\n    remote.systemPreferences.on('accent-color-changed', updateAccentColor);\n\n    // Return () => {\n    //   api.systemPreferences.off('accent-color-changed', updateAccentColor);\n    // };\n  }, []);\n\n  return (\n    <style jsx global>{`\n      html {\n        font-size: 62.5%;\n      }\n\n      body,\n      .cover-window {\n        margin: 0;\n        width: 100vw;\n        height: 100vh;\n        font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', sans-serif;\n        -webkit-font-smoothing: antialiased;\n        display: flex;\n        flex-direction: column;\n        overflow: hidden;\n      }\n\n      :root {\n            --kap-blue: #007aff;\n            --kap-blue-dark: #006be0;\n            --kap-blue-light: #3287ff;\n\n            --kap: var(--kap-blue);\n            --accent: #${accentColor};\n\n            --red: #ff3b30;\n            --black: #000000;\n            --dust: #606060;\n            --storm: #808080;\n            --cloud: #dddddd;\n            --smoke: #f1f1f1;\n            --white: #ffffff;\n\n            --window-background-color: var(--white);\n\n            --window-header-background: linear-gradient(-180deg, #f9f9f9 0%, #f1f1f1 100%);\n            --window-header-box-shadow: 0 1px 0 0 #ddd, inset 0 1px 0 0 #fff;\n\n            --title-color: var(--black);\n            --subtitle-color: var(--dust);\n            --link-color: var(--kap);\n\n            --row-divider-color: #f1f1f1;\n            --input-background-color: var(--white);\n            --input-border-color: var(--cloud);\n            --input-hover-border-color: #ccc;\n            --input-shadow: none;\n\n            --icon-focus-background-color: var(--smoke);\n            --icon-color: var(--storm);\n            --icon-hover-color: var(--dust);\n            --switch-box-shadow: none;\n            --switch-disabled-color: #ccc;\n            --shortcut-key-background: #fff;\n            --shortcut-key-border: #dddddd;\n            --shortcut-box-shadow: none;\n            --input-focus-border-color: #ccc;\n            --cropper-button-background-color: transparent;\n\n            ${systemColors}\n          }\n\n          .dark {\n            --kap: #fff;\n            --window-background-color: rgba(0, 0, 0, 0.3);\n\n            --window-header-background: linear-gradient(-180deg, rgba(68, 68, 68, 0.8) 0%, rgba(51, 51, 51, 0.8) 100%);\n            --window-header-box-shadow: inset 0 -1px 0 0 rgba(96, 96, 96, 0.2), 0 1px 0 0 #000000;\n\n            --title-color: var(--white);\n            --subtitle-color: #ffffff99;\n\n            --cropper-button-background-color: var(--input-background-color);\n            --row-divider-color: rgba(0, 0, 0, 0.2);\n            --input-background-color: rgba(255, 255, 255, 0.1);\n            --input-border-color: transparent;\n            --input-shadow: 0 0 1px 0 rgba(0,0,0,0.40), 0 1px 1px 0 rgba(0,0,0,0.10), inset 0 1px 0 0 rgba(255,255,255,0.10);\n\n            --icon-focus-background-color: rgba(255, 255, 255, 0.1);\n            --icon-hover-color: var(--cloud);\n            --switch-box-shadow: inset 0px 1px 0px 0px rgba(255, 255, 255, 0.04), 0px 1px 2px 0px rgba(0, 0, 0, 0.2), 0px 1px 0px 0px rgba(0, 0, 0, 0.1);\n            --switch-disabled-color: #666;\n            --shortcut-key-border: transparent;\n            --shortcut-key-background: #606060;\n            --shortcut-box-shadow: 0 0 1px 0 rgba(0,0,0,0.40), 0 1px 1px 0 rgba(0,0,0,0.10), inset 0 1px 0 0 rgba(255,255,255,0.10);\n            --input-hover-border-color: #666;\n            --input-focus-border-color: rgba(255, 255, 255, 0.2);\n          }\n\n          @keyframes shake-left {\n            10%,\n            90% {\n              transform: translate3d(-1px, 0, 0);\n            }\n\n            20%,\n            80% {\n              transform: translate3d(0, 0, 0);\n            }\n\n            30%,\n            50%,\n            70% {\n              transform: translate3d(-4px, 0, 0);\n            }\n\n            40%,\n            60% {\n              transform: translate3d(0, 0, 0);\n            }\n          }\n\n          @keyframes shake-right {\n            10%,\n            90% {\n              transform: translate3d(1px, 0, 0);\n            }\n\n            20%,\n            80% {\n              transform: translate3d(0, 0, 0);\n            }\n\n            30%,\n            50%,\n            70% {\n              transform: translate3d(4px, 0, 0);\n            }\n\n            40%,\n            60% {\n              transform: translate3d(0, 0, 0);\n            }\n          }\n\n          .shake-left {\n            transform: translate3d(0, 0, 0);\n            backface-visibility: hidden;\n            perspective: 1000px;\n            animation: shake-left 0.82s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;\n          }\n\n          .shake-right {\n            transform: translate3d(0, 0, 0);\n            backface-visibility: hidden;\n            perspective: 1000px;\n            animation: shake-right 0.82s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;\n          }\n\n          @keyframes shake {\n            10%,\n            90% {\n              transform: translate3d(-1px, 0, 0);\n            }\n\n            20%,\n            80% {\n              transform: translate3d(2px, 0, 0);\n            }\n\n            30%,\n            50%,\n            70% {\n              transform: translate3d(-4px, 0, 0);\n            }\n\n            40%,\n            60% {\n              transform: translate3d(4px, 0, 0);\n            }\n          }\n\n          .shake {\n            transform: translate3d(0, 0, 0);\n            backface-visibility: hidden;\n            perspective: 1000px;\n            animation: shake 0.82s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;\n          }\n\n          * { box-sizing: border-box; }\n    `}</style>\n  );\n};\n\nexport default GlobalStyles;\n\nconst systemColorNames = [\n  'control-background',\n  'control',\n  'control-text',\n  'disabled-control-text',\n  'find-highlight',\n  'grid',\n  'header-text',\n  'highlight',\n  'keyboard-focus-indicator',\n  'label',\n  'link',\n  'placeholder-text',\n  'quaternary-label',\n  'scrubber-textured-background',\n  'secondary-label',\n  'selected-content-background',\n  'selected-control',\n  'selected-control-text',\n  'selected-menu-item-text',\n  'selected-text-background',\n  'selected-text',\n  'separator',\n  'shadow',\n  'tertiary-label',\n  'text-background',\n  'text',\n  'under-page-background',\n  'unemphasized-selected-content-background',\n  'unemphasized-selected-text-background',\n  'unemphasized-selected-text',\n  'window-background',\n  'window-frame-text'\n];\n"
  },
  {
    "path": "renderer/utils/inputs.js",
    "content": "import electron from 'electron';\nimport _ from 'lodash';\n\nlet screenWidth = 0;\nlet screenHeight = 0;\n\nexport const setScreenSize = (width, height) => {\n  screenWidth = width;\n  screenHeight = height;\n};\n\nconst {remote} = electron;\nconst debounceTimeout = 500;\nexport const minWidth = 20;\nexport const minHeight = 20;\n\nexport const shake = (element, {className = 'shake'} = {}) => {\n  element.classList.add(className);\n\n  element.addEventListener('animationend', () => {\n    element.classList.remove(className);\n  }, {once: true});\n\n  return true;\n};\n\nexport const resizeTo = (bounds, target) => {\n  const {x, y} = bounds;\n  return {\n    width: target.width,\n    x: Math.min(x, screenWidth - target.width),\n    height: target.height,\n    y: Math.min(y, screenHeight - target.height)\n  };\n};\n\nconst handleWidthInput = _.debounce(({\n  bounds,\n  setBounds,\n  ratioLocked,\n  ratio,\n  value,\n  widthInput,\n  heightInput,\n  ignoreEmpty = true\n}) => {\n  const target = {};\n\n  if (value === '' && ignoreEmpty) {\n    return;\n  }\n\n  if (/^\\d+$/.test(value)) {\n    const integer = Number.parseInt(value, 10);\n\n    target.width = Math.max(minWidth, Math.min(screenWidth, integer));\n    if (target.width !== integer) {\n      shake(widthInput.current);\n    }\n\n    if (ratioLocked) {\n      const computedHeight = Math.ceil(target.width * ratio[1] / ratio[0]);\n      target.height = Math.max(minHeight, Math.min(screenHeight, computedHeight));\n\n      if (target.height !== computedHeight) {\n        shake(widthInput.current);\n        shake(heightInput.current);\n        target.width = Math.ceil(target.height * ratio[0] / ratio[1]);\n      }\n    } else if (bounds.height) {\n      target.height = bounds.height;\n    } else {\n      target.height = minHeight;\n    }\n\n    setBounds(resizeTo(bounds, target));\n  } else {\n    // If it's not an integer keep last valid value\n    shake(widthInput.current);\n    setBounds();\n  }\n}, debounceTimeout);\n\nconst handleHeightInput = _.debounce(({\n  bounds,\n  setBounds,\n  ratioLocked,\n  ratio,\n  value,\n  widthInput,\n  heightInput,\n  ignoreEmpty = true\n}) => {\n  const target = {};\n\n  if (value === '' && ignoreEmpty) {\n    return;\n  }\n\n  if (/^\\d+$/.test(value)) {\n    const integer = Number.parseInt(value, 10);\n\n    target.height = Math.max(minHeight, Math.min(screenHeight, integer));\n    if (target.height !== integer) {\n      shake(heightInput.current);\n    }\n\n    if (ratioLocked) {\n      const computedWidth = Math.ceil(target.height * ratio[0] / ratio[1]);\n      target.width = Math.max(minWidth, Math.min(screenWidth, computedWidth));\n\n      if (target.width !== computedWidth) {\n        shake(widthInput.current);\n        shake(heightInput.current);\n        target.height = Math.ceil(target.width * ratio[1] / ratio[0]);\n      }\n    } else if (bounds.width) {\n      target.width = bounds.width;\n    } else {\n      target.width = minWidth;\n    }\n\n    setBounds(resizeTo(bounds, target));\n  } else {\n    // If it's not an integer keep last valid value\n    shake(heightInput.current);\n    setBounds();\n  }\n}, debounceTimeout);\n\nexport const RATIOS = [\n  '16:9',\n  '5:4',\n  '5:3',\n  '4:3',\n  '3:2',\n  '1:1'\n];\n\nconst buildAspectRatioMenu = ({setRatio, ratio}) => {\n  if (!remote) {\n    return;\n  }\n\n  const {Menu, MenuItem} = remote;\n  const selectedRatio = ratio.join(':');\n  const menu = new Menu();\n\n  for (const r of RATIOS) {\n    menu.append(\n      new MenuItem({\n        label: r,\n        type: 'radio',\n        checked: r === selectedRatio,\n        click: () => setRatio(r.split(':').map(d => Number.parseInt(d, 10)))\n      })\n    );\n  }\n\n  const customOption = RATIOS.includes(selectedRatio) ? {\n    label: 'Custom',\n    type: 'radio',\n    checked: false,\n    enabled: false\n  } : {\n    label: `Custom ${selectedRatio}`,\n    type: 'radio',\n    checked: true\n  };\n\n  menu.append(new MenuItem(customOption));\n  return menu;\n};\n\nconst handleInputKeyPress = (onChange, min, max) => event => {\n  if (event.key === 'Enter') {\n    return onChange(event, {ignoreEmpty: false});\n  }\n\n  // Don't let shift key lock aspect ratio\n  if (event.key === 'Shift') {\n    event.stopPropagation();\n  }\n\n  const multiplier = event.shiftKey ? 10 : 1;\n  const parsedValue = Number.parseInt(event.currentTarget.value, 10);\n  if (Number.isNaN(parsedValue)) {\n    return;\n  }\n\n  // Fake an onChange event\n  if (event.key === 'ArrowUp') {\n    event.currentTarget.value = `${Math.min(parsedValue + multiplier, max)}`;\n    onChange(event);\n  } else if (event.key === 'ArrowDown') {\n    event.currentTarget.value = `${Math.max(parsedValue - multiplier, min)}`;\n    onChange(event);\n  }\n};\n\nconst handleKeyboardActivation = (onClick, {isMenu} = {}) => event => {\n  if (\n    (isMenu && event.key === 'ArrowDown') ||\n    (!isMenu && ['Enter', ' '].includes(event.key))\n  ) {\n    event.preventDefault();\n    if (onClick) {\n      onClick(event);\n    }\n  }\n};\n\nexport {\n  handleWidthInput,\n  handleHeightInput,\n  buildAspectRatioMenu,\n  handleInputKeyPress,\n  handleKeyboardActivation\n};\n"
  },
  {
    "path": "renderer/utils/sentry-error-boundary.tsx",
    "content": "import React from 'react';\nimport * as Sentry from '@sentry/browser';\nimport electron from 'electron';\nimport type {api as Api, is as Is} from 'electron-util';\n\nconst SENTRY_PUBLIC_DSN = 'https://2dffdbd619f34418817f4db3309299ce@sentry.io/255536';\n\nclass SentryErrorBoundary extends React.Component<{children: React.ReactNode}> {\n  constructor(props) {\n    super(props);\n    const {settings} = electron.remote.require('./common/settings');\n    // Done in-line because this is used in _app\n    const {is, api} = require('electron-util') as {\n      api: typeof Api;\n      is: typeof Is;\n    };\n\n    if (!is.development && settings.get('allowAnalytics')) {\n      const release = `${api.app.name}@${api.app.getVersion()}`.toLowerCase();\n      Sentry.init({dsn: SENTRY_PUBLIC_DSN, release});\n    }\n  }\n\n  componentDidCatch(error, errorInfo) {\n    console.log(error, errorInfo);\n    Sentry.configureScope(scope => {\n      for (const [key, value] of Object.entries(errorInfo)) {\n        scope.setExtra(key, value);\n      }\n    });\n\n    Sentry.captureException(error);\n\n    // This is needed to render errors correctly in development / production\n    super.componentDidCatch(error, errorInfo);\n  }\n\n  render() {\n    return this.props.children;\n  }\n}\n\nexport default SentryErrorBoundary;\n"
  },
  {
    "path": "renderer/utils/window.ts",
    "content": "\nexport const resizeKeepingCenter = (\n  bounds: Electron.Rectangle,\n  newSize: {width: number; height: number}\n): Electron.Rectangle => {\n  const cx = Math.round(bounds.x + (bounds.width / 2));\n  const cy = Math.round(bounds.y + (bounds.height / 2));\n\n  return {\n    x: Math.round(cx - (newSize.width / 2)),\n    y: Math.round(cy - (newSize.height / 2)),\n    width: newSize.width,\n    height: newSize.height\n  };\n};\n"
  },
  {
    "path": "renderer/vectors/applications.js",
    "content": "import React from 'react';\nimport Svg from './svg';\n\n// eslint-disable-next-line unicorn/prevent-abbreviations\nconst ApplicationsIcon = props => (\n  <Svg {...props}>\n    <path d=\"M19 4a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h14m0 14V8H5v10h14z\"/>\n  </Svg>\n);\n\nexport default ApplicationsIcon;\n"
  },
  {
    "path": "renderer/vectors/back-plain.tsx",
    "content": "import React, {FunctionComponent} from 'react';\nimport Svg, {SvgProps} from './svg';\n\nconst BackPlainIcon: FunctionComponent<SvgProps> = props => (\n  <Svg {...props}>\n    <path d=\"M0 0h24v24H0V0z\" fill=\"none\"/>\n    <path d=\"M15.4 16.6L10.8 12l4.6-4.6L14 6l-6 6 6 6 1.4-1.4z\"/>\n  </Svg>\n);\n\nexport default BackPlainIcon;\n"
  },
  {
    "path": "renderer/vectors/back.js",
    "content": "import React from 'react';\nimport Svg from './svg';\n\nconst BackIcon = props => (\n  <Svg {...props}>\n    <path d=\"M20 11v2H8l5.5 5.5-1.42 1.42L4.16 12l7.92-7.92L13.5 5.5 8 11h12z\"/>\n  </Svg>\n);\n\nexport default BackIcon;\n"
  },
  {
    "path": "renderer/vectors/cancel.js",
    "content": "import React from 'react';\nimport Svg from './svg';\n\nconst CancelIcon = props => (\n  <Svg {...props}>\n    <path d=\"M19 6.4L17.6 5 12 10.6 6.4 5 5 6.4l5.6 5.6L5 17.6 6.4 19l5.6-5.6 5.6 5.6 1.4-1.4-5.6-5.6L19 6.4z\"/>\n  </Svg>\n);\n\nexport default CancelIcon;\n"
  },
  {
    "path": "renderer/vectors/crop.js",
    "content": "import React from 'react';\nimport Svg from './svg';\n\nconst CropIcon = props => (\n  <Svg {...props}>\n    <path d=\"M7 17V1H5v4H1v2h4v10a2 2 0 0 0 2 2h10v4h2v-4h4v-2m-6-2h2V7a2 2 0 0 0-2-2H9v2h8v8z\"/>\n  </Svg>\n);\n\nexport default CropIcon;\n"
  },
  {
    "path": "renderer/vectors/dropdown-arrow.js",
    "content": "import React from 'react';\nimport Svg from './svg';\n\nconst DropdownArrowIcon = props => (\n  <Svg {...props}>\n    <path d=\"M7 10l5 5 5-5H7z\"/>\n  </Svg>\n);\n\nexport default DropdownArrowIcon;\n"
  },
  {
    "path": "renderer/vectors/edit.js",
    "content": "import React from 'react';\nimport Svg from './svg';\n\nconst EditIcon = props => (\n  <Svg {...props}>\n    <path d=\"M3 17.3V21h3.8l11-11-3.7-3.8L3 17.2zM20.7 7c.4-.4.4-1 0-1.4l-2.3-2.3a1 1 0 0 0-1.4 0L15 5 19 9 20.7 7z\"/>\n    <path d=\"M0 0h24v24H0z\" fill=\"none\"/>\n  </Svg>\n);\n\nexport default EditIcon;\n"
  },
  {
    "path": "renderer/vectors/error.js",
    "content": "import React from 'react';\nimport Svg from './svg';\n\nconst ErrorIcon = props => (\n  <Svg {...props}>\n    <path opacity=\".3\" d=\"M12 4a8 8 0 1 0 0 16 8 8 0 0 0 0-16zm1 13h-2v-2h2v2zm0-4h-2V7h2v6z\"/>\n    <path d=\"M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20zm0 18a8 8 0 1 1 0-16 8 8 0 0 1 0 16z\"/>\n    <path d=\"M11 15h2v2h-2zM11 7h2v6h-2z\"/>\n  </Svg>\n);\n\nexport default ErrorIcon;\n"
  },
  {
    "path": "renderer/vectors/exit-fullscreen.js",
    "content": "import React from 'react';\nimport Svg from './svg';\n\nconst ExitFullscreenIcon = props => (\n  <Svg {...props}>\n    <path d=\"M14 14h5v2h-3v3h-2v-5m-9 0h5v5H8v-3H5v-2m3-9h2v5H5V8h3V5m11 3v2h-5V5h2v3h3z\"/>\n  </Svg>\n);\n\nexport default ExitFullscreenIcon;\n"
  },
  {
    "path": "renderer/vectors/fullscreen.js",
    "content": "import React from 'react';\nimport Svg from './svg';\n\nconst FullscrenIcon = props => (\n  <Svg {...props}>\n    <path d=\"M3 5v4h2V5h4V3H5a2 2 0 0 0-2 2zm2 10H3v4c0 1.1.9 2 2 2h4v-2H5v-4zm14 4h-4v2h4a2 2 0 0 0 2-2v-4h-2v4zm0-16h-4v2h4v4h2V5a2 2 0 0 0-2-2z\"/>\n  </Svg>\n);\n\nexport default FullscrenIcon;\n"
  },
  {
    "path": "renderer/vectors/gear.js",
    "content": "import React from 'react';\nimport Svg from './svg';\n\nconst GearIcon = props => (\n  <Svg {...props}>\n    <path d=\"M12 15.5A3.5 3.5 0 0 1 8.5 12 3.5 3.5 0 0 1 12 8.5a3.5 3.5 0 0 1 3.5 3.5 3.5 3.5 0 0 1-3.5 3.5m7.43-2.53a7.77 7.77 0 0 0 0-1.97l2.11-1.63a.5.5 0 0 0 .12-.64l-2-3.46c-.12-.22-.39-.31-.61-.22l-2.49 1a7.25 7.25 0 0 0-1.69-.98l-.37-2.65A.5.5 0 0 0 14 2h-4a.5.5 0 0 0-.5.42l-.37 2.65c-.63.25-1.17.59-1.69.98l-2.49-1a.5.5 0 0 0-.61.22l-2 3.46a.5.5 0 0 0 .12.64L4.57 11a7.77 7.77 0 0 0 0 1.97l-2.11 1.66a.5.5 0 0 0-.12.64l2 3.46c.12.22.39.3.61.22l2.49-1.01c.52.4 1.06.74 1.69.99l.37 2.65a.5.5 0 0 0 .5.42h4a.5.5 0 0 0 .5-.42l.37-2.65a7.28 7.28 0 0 0 1.69-.99l2.49 1.01a.5.5 0 0 0 .61-.22l2-3.46a.5.5 0 0 0-.12-.64l-2.11-1.66z\"/>\n  </Svg>\n);\n\nexport default GearIcon;\n"
  },
  {
    "path": "renderer/vectors/help.js",
    "content": "import React from 'react';\nimport Svg from './svg';\n\nconst HelpIcon = props => (\n  <Svg {...props}>\n    <path d=\"M0 0h24v24H0z\" fill=\"none\"/>\n    <path d=\"M12 2a10 10 0 100 20 10 10 0 000-20zm1 17h-2v-2h2v2zm2-7.8l-.8 1c-.8.7-1.2 1.3-1.2 2.8h-2v-.5a4 4 0 011.2-2.8l1.2-1.3c.4-.4.6-.9.6-1.4 0-1.1-.9-2-2-2s-2 .9-2 2H8a4 4 0 118 0c0 .9-.4 1.7-1 2.3z\"/>\n  </Svg>\n);\n\nexport default HelpIcon;\n"
  },
  {
    "path": "renderer/vectors/index.js",
    "content": "import ApplicationsIcon from './applications';\nimport BackIcon from './back';\nimport CropIcon from './crop';\nimport DropdownArrowIcon from './dropdown-arrow';\nimport FullscreenIcon from './fullscreen';\nimport LinkIcon from './link';\nimport SwapIcon from './swap';\nimport ExitFullscreenIcon from './exit-fullscreen';\nimport SettingsIcon from './settings';\nimport TuneIcon from './tune';\nimport PluginsIcon from './plugins';\nimport GearIcon from './gear';\nimport SpinnerIcon from './spinner';\nimport MoreIcon from './more';\nimport PlayIcon from './play';\nimport PauseIcon from './pause';\nimport VolumeHighIcon from './volume-high';\nimport VolumeOffIcon from './volume-off';\nimport CancelIcon from './cancel';\nimport TooltipIcon from './tooltip';\nimport EditIcon from './edit';\nimport ErrorIcon from './error';\nimport OpenConfigIcon from './open-config';\nimport OpenOnGithubIcon from './open-on-github';\nimport HelpIcon from './help';\nimport BackPlainIcon from './back-plain';\n\nexport {\n  ApplicationsIcon,\n  BackIcon,\n  CropIcon,\n  DropdownArrowIcon,\n  FullscreenIcon,\n  LinkIcon,\n  SwapIcon,\n  ExitFullscreenIcon,\n  SettingsIcon,\n  TuneIcon,\n  PluginsIcon,\n  GearIcon,\n  SpinnerIcon,\n  MoreIcon,\n  PlayIcon,\n  PauseIcon,\n  VolumeHighIcon,\n  VolumeOffIcon,\n  CancelIcon,\n  TooltipIcon,\n  EditIcon,\n  ErrorIcon,\n  OpenConfigIcon,\n  OpenOnGithubIcon,\n  HelpIcon,\n  BackPlainIcon\n};\n"
  },
  {
    "path": "renderer/vectors/link.js",
    "content": "import React from 'react';\nimport Svg from './svg';\n\nconst LinkIcon = props => (\n  <Svg {...props}>\n    <path d=\"M16 6h-3v1.9h3c2.3 0 4.1 1.8 4.1 4.1a4.1 4.1 0 0 1-4.1 4.1h-3V18h3a6 6 0 0 0 6-6 6 6 0 0 0-6-6M3.9 12c0-2.3 1.8-4.1 4.1-4.1h3V6H8a6 6 0 0 0-6 6 6 6 0 0 0 6 6h3v-1.9H8A4.1 4.1 0 0 1 3.9 12M8 13h8v-2H8v2z\"/>\n  </Svg>\n);\n\nexport default LinkIcon;\n"
  },
  {
    "path": "renderer/vectors/more.js",
    "content": "import React from 'react';\nimport Svg from './svg';\n\nconst MoreIcon = props => (\n  <Svg {...props}>\n    <path d=\"M0 0h24v24H0z\" fill=\"none\"/>\n    <path d=\"M6 10c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm12 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm-6 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z\"/>\n  </Svg>\n);\n\nexport default MoreIcon;\n"
  },
  {
    "path": "renderer/vectors/open-config.js",
    "content": "import React from 'react';\nimport Svg from './svg';\n\nconst OpenConfigIcon = props => (\n  <Svg {...props}>\n    <path d=\"M14 2H6a2 2 0 0 0-2 2v16c0 1.1.9 2 2 2h12a2 2 0 0 0 2-2V8l-6-6zM6 20V4h7v5h5v11H6z\"/>\n  </Svg>\n);\n\nexport default OpenConfigIcon;\n"
  },
  {
    "path": "renderer/vectors/open-on-github.js",
    "content": "import React from 'react';\nimport Svg from './svg';\n\nconst OpenOnGithubIcon = props => (\n  <Svg {...props}>\n    <path d=\"M12 .3a12 12 0 0 0-3.8 23.4c.6.1.8-.3.8-.6v-2c-3.3.7-4-1.6-4-1.6-.6-1.4-1.4-1.8-1.4-1.8-1-.7.1-.7.1-.7 1.2 0 1.9 1.2 1.9 1.2 1 1.8 2.8 1.3 3.5 1 0-.8.4-1.3.7-1.6-2.7-.3-5.5-1.3-5.5-6 0-1.2.5-2.3 1.3-3.1-.2-.4-.6-1.6 0-3.2 0 0 1-.3 3.4 1.2a11.5 11.5 0 0 1 6 0c2.3-1.5 3.3-1.2 3.3-1.2.6 1.6.2 2.8 0 3.2.9.8 1.3 1.9 1.3 3.2 0 4.6-2.8 5.6-5.5 5.9.5.4.9 1 .9 2.2v3.3c0 .3.1.7.8.6A12 12 0 0 0 12 .3\"/>\n  </Svg>\n);\n\nexport default OpenOnGithubIcon;\n"
  },
  {
    "path": "renderer/vectors/pause.js",
    "content": "import React from 'react';\nimport Svg from './svg';\n\nconst PauseIcon = props => (\n  <Svg {...props}>\n    <path d=\"M14,19H18V5H14M6,19H10V5H6V19Z\"/>\n  </Svg>\n);\n\nexport default PauseIcon;\n"
  },
  {
    "path": "renderer/vectors/play.js",
    "content": "import React from 'react';\nimport Svg from './svg';\n\nconst PlayIcon = props => (\n  <Svg {...props}>\n    <path d=\"M8,5.14V19.14L19,12.14L8,5.14Z\"/>\n  </Svg>\n);\n\nexport default PlayIcon;\n"
  },
  {
    "path": "renderer/vectors/plugins.js",
    "content": "import React from 'react';\nimport Svg from './svg';\n\nconst PluginsIcon = props => (\n  <Svg {...props}>\n    <path d=\"M19 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-2 10h-4v4h-2v-4H7v-2h4V7h2v4h4v2z\"/>\n  </Svg>\n);\n\nexport default PluginsIcon;\n"
  },
  {
    "path": "renderer/vectors/settings.js",
    "content": "import React from 'react';\nimport Svg from './svg';\n\nconst SettingsIcon = props => (\n  <Svg {...props}>\n    <path d=\"M0 0h24v24H0z\" fill=\"none\"/>\n    <path d=\"M12 10c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm7-7H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2zm-1.75 9c0 .23-.02.46-.05.68l1.48 1.16c.13.11.17.3.08.45l-1.4 2.42c-.09.15-.27.21-.43.15l-1.74-.7c-.36.28-.76.51-1.18.69l-.26 1.85a.36.36 0 0 1-.35.3h-2.8a.36.36 0 0 1-.35-.29l-.26-1.85a5.14 5.14 0 0 1-1.18-.69l-1.74.7a.35.35 0 0 1-.43-.15l-1.4-2.42a.35.35 0 0 1 .08-.45l1.48-1.16a5.34 5.34 0 0 1 0-1.37l-1.48-1.16a.35.35 0 0 1-.08-.45l1.4-2.42c.09-.15.27-.21.43-.15l1.74.7c.36-.28.76-.51 1.18-.69l.26-1.85c.03-.17.18-.3.35-.3h2.8c.17 0 .32.13.35.29l.26 1.85c.43.18.82.41 1.18.69l1.74-.7c.16-.06.34 0 .43.15l1.4 2.42c.09.15.05.34-.08.45l-1.48 1.16c.03.23.05.46.05.69z\"/>\n  </Svg>\n);\n\nexport default SettingsIcon;\n"
  },
  {
    "path": "renderer/vectors/spinner.js",
    "content": "import PropTypes from 'prop-types';\nimport React from 'react';\n\nconst SpinnerIcon = ({stroke = 'var(--kap)'}) => (\n  <svg viewBox=\"0 0 16 16\">\n    <circle cx=\"8\" cy=\"8\" r=\"7\" strokeWidth=\"1\" fill=\"none\"/>\n    <style jsx>{`\n      circle {\n        fill: transparent;\n        stroke: ${stroke};\n        stroke-linecap: round;\n        stroke-dasharray: calc(3.14px * 16);\n        stroke-dashoffset: 16;\n        animation: spinner 3s linear infinite;\n      }\n\n      @keyframes spinner {\n          0% {\n              stroke-dashoffset: 10.56;\n          }\n          50% {\n              stroke-dashoffset: 50.24;\n          }\n          100% {\n              stroke-dashoffset: 0.66;\n          }\n      }\n    `}</style>\n  </svg>\n);\n\nSpinnerIcon.propTypes = {\n  stroke: PropTypes.string\n};\n\nexport default SpinnerIcon;\n"
  },
  {
    "path": "renderer/vectors/svg.tsx",
    "content": "import React, {FunctionComponent} from 'react';\nimport classNames from 'classnames';\n\nimport {handleKeyboardActivation} from '../utils/inputs';\n\nconst defaultProps: SvgProps = {\n  fill: 'var(--icon-color)',\n  activeFill: 'var(--kap)',\n  hoverFill: 'var(--icon-hover-color)',\n  size: '24px',\n  active: false,\n  viewBox: '0 0 24 24',\n  tabIndex: -1\n};\n\nconst stopPropagation = event => {\n  event.stopPropagation();\n};\n\nconst Svg: FunctionComponent<SvgProps> = props => {\n  const {\n    fill,\n    size,\n    activeFill,\n    hoverFill,\n    active,\n    onClick,\n    children,\n    viewBox,\n    shadow,\n    tabIndex,\n    isMenu\n  } = {\n    ...defaultProps,\n    ...props\n  };\n\n  const className = classNames({active, shadow, focusable: tabIndex >= 0});\n\n  return (\n    <div tabIndex={tabIndex} onKeyDown={tabIndex >= 0 ? handleKeyboardActivation(onClick, {isMenu}) : undefined}>\n      <svg\n        viewBox={viewBox}\n        className={className}\n        onClick={onClick}\n        onMouseDown={stopPropagation}\n      >\n        {children}\n      </svg>\n      <style jsx>{`\n            svg {\n              fill: ${fill};\n              width: ${size};\n              height: ${size};\n            }\n\n            svg:hover,\n            div:focus svg {\n              fill: ${hoverFill};\n            }\n\n            div {\n              position: relative;\n              width: ${size};\n              height: ${size};\n              outline: none;\n            }\n\n            div.focusable:focus::before {\n              content: '';\n              position: absolute;\n              left: 0;\n              right: 0;\n              width: 100%;\n              height: 100%;\n              transform: scale(${1 / 0.75});\n              background: var(--icon-focus-background-color);\n              z-index: -1;\n              border-radius: 2px;\n            }\n\n            .shadow {\n              filter: drop-shadow(0 1px 2px rgba(0,0,0,.1));\n            }\n\n            .active,\n            .active:hover,\n            div.focusable:focus svg {\n              fill: ${activeFill};\n            }\n        `}</style>\n    </div>\n  );\n};\n\nexport interface SvgProps {\n  fill?: string;\n  size?: string;\n  activeFill?: string;\n  hoverFill?: string;\n  active?: boolean;\n  viewBox?: string;\n  onClick?: () => void;\n  shadow?: boolean;\n  tabIndex?: number;\n  isMenu?: boolean;\n}\n\nexport default Svg;\n"
  },
  {
    "path": "renderer/vectors/swap.js",
    "content": "import React from 'react';\nimport Svg from './svg';\n\nconst SwapIcon = props => (\n  <Svg {...props}>\n    <path d=\"M21 9l-4-4v3h-7v2h7v3M7 11l-4 4 4 4v-3h7v-2H7v-3z\"/>\n  </Svg>\n);\n\nexport default SwapIcon;\n"
  },
  {
    "path": "renderer/vectors/tooltip.js",
    "content": "import React from 'react';\nimport Svg from './svg';\n\nconst TooltipIcon = props => (\n  <Svg {...props}>\n    <path d=\"M590,700 C588.089728,700 584.354886,703.64914 578.795474,710.94742 L578.795448,710.9474 C578.460795,711.386725 577.833363,711.471579 577.394038,711.136926 C577.322643,711.082542 577.258896,711.018794 577.204512,710.9474 C571.645108,703.649133 567.910271,700 566,700 L590,700 Z\" transform=\"translate(-566 -700)\"/>\n  </Svg>\n);\n\nexport default TooltipIcon;\n"
  },
  {
    "path": "renderer/vectors/tune.js",
    "content": "import React from 'react';\nimport Svg from './svg';\n\nconst TuneIcon = props => (\n  <Svg {...props}>\n    <path d=\"M0 0h24v24H0z\" fill=\"none\"/>\n    <path d=\"M3 17v2h6v-2H3zM3 5v2h10V5H3zm10 16v-2h8v-2h-8v-2h-2v6h2zM7 9v2H3v2h4v2h2V9H7zm14 4v-2H11v2h10zm-6-4h2V7h4V5h-4V3h-2v6z\"/>\n  </Svg>\n);\n\nexport default TuneIcon;\n"
  },
  {
    "path": "renderer/vectors/volume-high.js",
    "content": "import React from 'react';\nimport Svg from './svg';\n\nconst VolumeHighIcon = props => (\n  <Svg {...props}>\n    <path d=\"M14,3.23V5.29C16.89,6.15 19,8.83 19,12C19,15.17 16.89,17.84 14,18.7V20.77C18,19.86 21,16.28 21,12C21,7.72 18,4.14 14,3.23M16.5,12C16.5,10.23 15.5,8.71 14,7.97V16C15.5,15.29 16.5,13.76 16.5,12M3,9V15H7L12,20V4L7,9H3Z\"/>\n  </Svg>\n);\n\nexport default VolumeHighIcon;\n"
  },
  {
    "path": "renderer/vectors/volume-off.js",
    "content": "import React from 'react';\nimport Svg from './svg';\n\nconst VolumeOffIcon = props => (\n  <Svg {...props}>\n    <path d=\"M12,4L9.91,6.09L12,8.18M4.27,3L3,4.27L7.73,9H3V15H7L12,20V13.27L16.25,17.53C15.58,18.04 14.83,18.46 14,18.7V20.77C15.38,20.45 16.63,19.82 17.68,18.96L19.73,21L21,19.73L12,10.73M19,12C19,12.94 18.8,13.82 18.46,14.64L19.97,16.15C20.62,14.91 21,13.5 21,12C21,7.72 18,4.14 14,3.23V5.29C16.89,6.15 19,8.83 19,12M16.5,12C16.5,10.23 15.5,8.71 14,7.97V10.18L16.45,12.63C16.5,12.43 16.5,12.21 16.5,12Z\"/>\n  </Svg>\n);\n\nexport default VolumeOffIcon;\n"
  },
  {
    "path": "test/convert.ts",
    "content": "import {serial as testAny, TestInterface} from 'ava';\nimport fs from 'fs';\nimport path from 'path';\nimport sinon from 'sinon';\nimport uniqueString from 'unique-string';\n\nconst test = testAny as TestInterface<{outputPath: string}>;\n\nimport {getVideoMetadata} from './helpers/video-utils';\nimport {almostEquals} from './helpers/assertions';\nimport {getFormatExtension} from '../main/common/constants';\nimport {Except, SetOptional} from 'type-fest';\nimport {mockImport} from './helpers/mocks';\nimport {Format} from '../main/common/types';\n\nconst getRandomFileName = (ext: Format = Format.mp4) => `${uniqueString()}.${getFormatExtension(ext)}`;\n\nconst input = path.resolve(__dirname, 'fixtures', 'input.mp4');\nconst retinaInput = path.resolve(__dirname, 'fixtures', 'input@2x.mp4');\n\nmockImport('../common/analytics', 'analytics');\nmockImport('../plugins/service-context', 'service-context');\nmockImport('../plugins', 'plugins');\nconst {settings} = mockImport('../common/settings', 'settings');\n\nimport {convertTo} from '../main/converters';\nimport {ConvertOptions} from '../main/converters/utils';\n\ntest.afterEach.always(t => {\n  if (t.context.outputPath && fs.existsSync(t.context.outputPath)) {\n    fs.unlinkSync(t.context.outputPath);\n  }\n});\n\nconst convert = async (format: Format, options: SetOptional<Except<ConvertOptions, 'outputPath'>, 'onCancel' | 'onProgress' | 'shouldMute'>) => {\n  return convertTo(format, {\n    defaultFileName: getRandomFileName(format),\n    onProgress: sinon.fake(),\n    onCancel: sinon.fake(),\n    shouldMute: true,\n    ...options\n  });\n};\n\n// MP4\n\ntest('mp4: retina with sound', async t => {\n  const onProgress = sinon.fake();\n\n  t.context.outputPath = await convert(Format.mp4, {\n    shouldMute: false,\n    inputPath: retinaInput,\n    fps: 39,\n    width: 469,\n    height: 839,\n    startTime: 30,\n    endTime: 43.5,\n    shouldCrop: true,\n    onProgress\n  });\n\n  const meta = await getVideoMetadata(t.context.outputPath);\n\n  // Makes dimensions even\n  t.is(meta.size.width, 470);\n  t.is(meta.size.height, 840);\n\n  t.is(meta.fps, 39);\n  t.true(almostEquals(meta.duration, 13.5));\n  t.is(meta.encoding, 'h264');\n\n  t.true(meta.hasAudio);\n\n  t.true(onProgress.calledWithMatch(sinon.match.string, sinon.match.number));\n  t.true(onProgress.calledWithMatch(sinon.match.string, sinon.match.number, sinon.match.string));\n});\n\ntest('mp4: retina without sound', async t => {\n  t.context.outputPath = await convert(Format.mp4, {\n    shouldMute: true,\n    inputPath: retinaInput,\n    fps: 10,\n    width: 46,\n    height: 83,\n    startTime: 0,\n    endTime: 5,\n    shouldCrop: true\n  });\n\n  const meta = await getVideoMetadata(t.context.outputPath);\n\n  t.false(meta.hasAudio);\n});\n\ntest('mp4: non-retina', async t => {\n  t.context.outputPath = await convert(Format.mp4, {\n    shouldMute: false,\n    inputPath: input,\n    fps: 30,\n    width: 255,\n    height: 143,\n    startTime: 11.5,\n    endTime: 27,\n    // Should resize even though this is false, because dimensions are odd\n    shouldCrop: false\n  });\n\n  const meta = await getVideoMetadata(t.context.outputPath);\n\n  // Makes dimensions even\n  t.is(meta.size.width, 256);\n  t.is(meta.size.height, 144);\n\n  t.is(meta.fps, 30);\n  t.true(almostEquals(meta.duration, 15.5));\n  t.is(meta.encoding, 'h264');\n\n  t.false(meta.hasAudio);\n});\n\n// WEBM\n\ntest('webm: retina with sound', async t => {\n  const onProgress = sinon.fake();\n\n  t.context.outputPath = await convert(Format.webm, {\n    shouldMute: false,\n    inputPath: retinaInput,\n    fps: 39,\n    width: 469,\n    height: 839,\n    startTime: 30,\n    endTime: 43.5,\n    shouldCrop: true,\n    onProgress\n  });\n\n  const meta = await getVideoMetadata(t.context.outputPath);\n\n  t.is(meta.size.width, 470);\n  t.is(meta.size.height, 840);\n\n  t.is(meta.fps, 39);\n  t.true(almostEquals(meta.duration, 13.5));\n  t.is(meta.encoding, 'vp9');\n\n  t.true(meta.hasAudio);\n\n  t.true(onProgress.calledWithMatch(sinon.match.string, sinon.match.number));\n  t.true(onProgress.calledWithMatch(sinon.match.string, sinon.match.number, sinon.match.string));\n});\n\ntest('webm: retina without sound', async t => {\n  t.context.outputPath = await convert(Format.webm, {\n    shouldMute: true,\n    inputPath: retinaInput,\n    fps: 10,\n    width: 46,\n    height: 83,\n    startTime: 0,\n    endTime: 5,\n    shouldCrop: true\n  });\n\n  const meta = await getVideoMetadata(t.context.outputPath);\n\n  t.false(meta.hasAudio);\n});\n\ntest('webm: non-retina', async t => {\n  t.context.outputPath = await convert(Format.webm, {\n    shouldMute: false,\n    inputPath: input,\n    fps: 30,\n    width: 255,\n    height: 143,\n    startTime: 11.5,\n    endTime: 27,\n    shouldCrop: true\n  });\n\n  const meta = await getVideoMetadata(t.context.outputPath);\n\n  t.is(meta.size.width, 256);\n  t.is(meta.size.height, 144);\n\n  t.is(meta.fps, 30);\n  t.true(almostEquals(meta.duration, 15.5));\n  t.is(meta.encoding, 'vp9');\n\n  t.false(meta.hasAudio);\n});\n\n// APNG\n\ntest('apng: retina', async t => {\n  const onProgress = sinon.fake();\n\n  t.context.outputPath = await convert(Format.apng, {\n    shouldMute: false,\n    inputPath: retinaInput,\n    fps: 15,\n    width: 469,\n    height: 839,\n    startTime: 30,\n    endTime: 43.5,\n    shouldCrop: true,\n    onProgress\n  });\n\n  const meta = await getVideoMetadata(t.context.outputPath);\n\n  t.is(meta.size.width, 469);\n  t.is(meta.size.height, 839);\n\n  t.is(meta.fps, 15);\n  t.is(meta.encoding, 'apng');\n\n  t.false(meta.hasAudio);\n\n  t.true(onProgress.calledWithMatch(sinon.match.string, sinon.match.number));\n  t.true(onProgress.calledWithMatch(sinon.match.string, sinon.match.number, sinon.match.string));\n});\n\ntest('apng: non-retina', async t => {\n  t.context.outputPath = await convert(Format.apng, {\n    shouldMute: false,\n    inputPath: input,\n    fps: 15,\n    width: 255,\n    height: 143,\n    startTime: 11.5,\n    endTime: 27,\n    shouldCrop: true\n  });\n\n  const meta = await getVideoMetadata(t.context.outputPath);\n\n  t.is(meta.size.width, 255);\n  t.is(meta.size.height, 143);\n\n  t.is(meta.fps, 15);\n  t.is(meta.encoding, 'apng');\n\n  t.false(meta.hasAudio);\n});\n\n// GIF\n\ntest('gif: retina', async t => {\n  const onProgress = sinon.fake();\n\n  t.context.outputPath = await convert(Format.gif, {\n    shouldMute: false,\n    inputPath: retinaInput,\n    fps: 10,\n    width: 236,\n    height: 420,\n    startTime: 0,\n    endTime: 8.5,\n    shouldCrop: true,\n    onProgress\n  });\n\n  const meta = await getVideoMetadata(t.context.outputPath);\n\n  t.is(meta.size.width, 236);\n  t.is(meta.size.height, 420);\n\n  t.is(meta.fps, 10);\n  t.true(almostEquals(meta.duration, 8.5));\n  t.is(meta.encoding, 'gif');\n\n  t.false(meta.hasAudio);\n\n  t.true(onProgress.calledWithMatch(sinon.match.string, sinon.match.number));\n  t.true(onProgress.calledWithMatch(sinon.match.string, sinon.match.number, sinon.match.string));\n});\n\ntest('gif: non-retina', async t => {\n  t.context.outputPath = await convert(Format.gif, {\n    shouldMute: false,\n    inputPath: input,\n    fps: 15,\n    width: 255,\n    height: 143,\n    startTime: 11.5,\n    endTime: 27,\n    shouldCrop: true\n  });\n\n  const meta = await getVideoMetadata(t.context.outputPath);\n\n  t.is(meta.size.width, 255);\n  t.is(meta.size.height, 143);\n\n  t.is(meta.fps, 15);\n  t.true(almostEquals(meta.duration, 15.5));\n  t.is(meta.encoding, 'gif');\n\n  t.false(meta.hasAudio);\n});\n\ntest('gif: lossy', async t => {\n  settings.setMock('lossyCompression', false);\n\n  const regular = await convert(Format.gif, {\n    inputPath: input,\n    fps: 20,\n    width: 510,\n    height: 286,\n    startTime: 1,\n    endTime: 10,\n    shouldCrop: true\n  });\n\n  settings.setMock('lossyCompression', true);\n\n  const lossy = await convert(Format.gif, {\n    inputPath: input,\n    fps: 20,\n    width: 510,\n    height: 286,\n    startTime: 1,\n    endTime: 10,\n    shouldCrop: true\n  });\n\n  t.true(\n    fs.statSync(regular).size >=\n    fs.statSync(lossy).size\n  );\n});\n\n// AV1\n\ntest('av1: retina with sound', async t => {\n  const onProgress = sinon.fake();\n\n  t.context.outputPath = await convert(Format.av1, {\n    shouldMute: false,\n    inputPath: retinaInput,\n    fps: 15,\n    width: 235,\n    height: 420,\n    startTime: 30,\n    endTime: 35.5,\n    shouldCrop: true,\n    onProgress\n  });\n\n  const meta = await getVideoMetadata(t.context.outputPath);\n\n  // Makes dimensions even\n  t.is(meta.size.width, 236);\n  t.is(meta.size.height, 420);\n\n  t.is(meta.fps, 15);\n  t.true(almostEquals(meta.duration, 5.5));\n  t.is(meta.encoding, 'av1');\n\n  t.true(meta.hasAudio);\n\n  t.true(onProgress.calledWithMatch(sinon.match.string, sinon.match.number));\n  t.true(onProgress.calledWithMatch(sinon.match.string, sinon.match.number, sinon.match.string));\n});\n\ntest('av1: retina without sound', async t => {\n  t.context.outputPath = await convert(Format.av1, {\n    shouldMute: true,\n    inputPath: retinaInput,\n    fps: 10,\n    width: 100,\n    height: 200,\n    startTime: 0,\n    endTime: 4,\n    shouldCrop: true\n  });\n\n  const meta = await getVideoMetadata(t.context.outputPath);\n\n  t.false(meta.hasAudio);\n});\n\ntest('av1: non-retina', async t => {\n  t.context.outputPath = await convert(Format.av1, {\n    shouldMute: false,\n    inputPath: input,\n    fps: 10,\n    width: 255,\n    height: 143,\n    startTime: 11.5,\n    endTime: 16,\n    shouldCrop: true\n  });\n\n  const meta = await getVideoMetadata(t.context.outputPath);\n\n  // Makes dimensions even\n  t.is(meta.size.width, 256);\n  t.is(meta.size.height, 144);\n\n  t.is(meta.fps, 10);\n  t.true(almostEquals(meta.duration, 4.5));\n  t.is(meta.encoding, 'av1');\n\n  t.false(meta.hasAudio);\n});\n\n// HEVC\n\ntest('HEVC: retina', async t => {\n  const onProgress = sinon.fake();\n\n  t.context.outputPath = await convert(Format.hevc, {\n    shouldMute: true,\n    inputPath: retinaInput,\n    fps: 15,\n    width: 469,\n    height: 839,\n    startTime: 30,\n    endTime: 43.5,\n    shouldCrop: true,\n    onProgress\n  });\n\n  const meta = await getVideoMetadata(t.context.outputPath);\n\n  // Makes dimensions even\n  t.is(meta.size.width, 470);\n  t.is(meta.size.height, 840);\n\n  t.is(meta.fps, 15);\n  t.is(meta.encoding, 'hevc');\n\n  t.false(meta.hasAudio);\n\n  t.true(onProgress.calledWithMatch(sinon.match.string, sinon.match.number));\n  t.true(onProgress.calledWithMatch(sinon.match.string, sinon.match.number, sinon.match.string));\n});\n\ntest('HEVC: non-retina', async t => {\n  t.context.outputPath = await convert(Format.hevc, {\n    shouldMute: true,\n    inputPath: input,\n    fps: 15,\n    width: 255,\n    height: 143,\n    startTime: 11.5,\n    endTime: 27,\n    shouldCrop: true\n  });\n\n  const meta = await getVideoMetadata(t.context.outputPath);\n\n  // Makes dimensions even\n  t.is(meta.size.width, 256);\n  t.is(meta.size.height, 144);\n\n  t.is(meta.fps, 15);\n  t.is(meta.encoding, 'hevc');\n\n  t.false(meta.hasAudio);\n});\n"
  },
  {
    "path": "test/helpers/assertions.ts",
    "content": "\nexport const almostEquals = (actual: number, expected: number, threshold = 0.5) => {\n  return Math.abs(actual - expected) <= threshold ? true : `\n  Actual: ${actual}\n  Expected: ${expected}\n  Threshold: ${threshold}\n  Diff: ${Math.abs(actual - expected)}\n  `;\n};\n"
  },
  {
    "path": "test/helpers/mocks.ts",
    "content": "import moduleAlias from 'module-alias';\nimport path from 'path';\nimport fs from 'fs';\n\nexport const mockModule = (name: string) => {\n  const mockModulePathTypescript = path.resolve(__dirname, '..', 'mocks', `${name}.ts`);\n  const mockModulePath = path.resolve(__dirname, '..', 'mocks', `${name}.js`);\n\n  const mockPath = [\n    mockModulePathTypescript,\n    mockModulePath\n  ].find(p => fs.existsSync(p));\n\n  if (!mockPath) {\n    throw new Error(`Missing mock implementation at ${mockModulePath}`.replace('js', '(ts|js)'));\n  }\n\n  moduleAlias.addAlias(name, mockPath);\n  return require(mockPath);\n};\n\nexport const mockImport = (importPath: string, mock: string) => {\n  const mockModulePathTypescript = path.resolve(__dirname, '..', 'mocks', `${mock}.ts`);\n  const mockModulePath = path.resolve(__dirname, '..', 'mocks', `${mock}.js`);\n\n  const mockPath = [\n    mockModulePathTypescript,\n    mockModulePath\n  ].find(p => fs.existsSync(p));\n\n  if (!mockPath) {\n    throw new Error(`Missing mock implementation at ${mockModulePath}`.replace('js', '(ts|js)'));\n  }\n\n  moduleAlias.addAlias(importPath, mockPath);\n  return require(mockPath);\n};\n"
  },
  {
    "path": "test/helpers/video-utils.ts",
    "content": "import moment from 'moment';\nimport execa from 'execa';\n\nconst ffmpegPath = require('ffmpeg-static');\n\nconst getDuration = (text: string): number => {\n  const durationString = /Duration: ([\\d:.]*)/.exec(text)?.[1];\n  return moment.duration(durationString).asSeconds();\n};\n\nconst getEncoding = (text: string) => /Stream.*Video: (.*?)[, ]/.exec(text)?.[1];\n\nconst getFps = (text: string) => {\n  const fpsString = /([\\d.]*) fps/.exec(text)?.[1];\n  return Number.parseFloat(fpsString!);\n};\n\nconst getSize = (text: string) => {\n  const sizeText = /Video:.*?, (\\d*x\\d*)/.exec(text)?.[1]!;\n  const parts = sizeText.split('x');\n  return {\n    width: Number.parseFloat(parts[0]),\n    height: Number.parseFloat(parts[1])\n  };\n};\n\nconst getHasAudio = (text: string) => /Stream #.*: Audio/.test(text);\n\n// @ts-expect-error\nexport const getVideoMetadata = async (path: string): Promise<{\n  duration: number;\n  encoding: string;\n  fps: number;\n  size: {width: number; height: number};\n  hasAudio: boolean;\n}> => {\n  try {\n    await execa(ffmpegPath, ['-i', path]);\n  } catch (error) {\n    const {stderr} = error as any;\n    return {\n      duration: getDuration(stderr),\n      encoding: getEncoding(stderr)!,\n      fps: getFps(stderr)!,\n      size: getSize(stderr) as {width: number; height: number},\n      hasAudio: getHasAudio(stderr)!\n    };\n  }\n};\n"
  },
  {
    "path": "test/mocks/analytics.ts",
    "content": "\nexport const initializeAnalytics = () => {};\nexport const track = () => {};\n"
  },
  {
    "path": "test/mocks/dialog.ts",
    "content": "import sinon from 'sinon';\n\nlet dialogState: any;\nlet dialogResolve: any;\nlet waitForDialogResolve: any;\n\nexport const showDialog = sinon.fake(async (options: any) => new Promise(resolve => {\n  dialogState = options;\n  dialogResolve = resolve;\n\n  if (waitForDialogResolve) {\n    waitForDialogResolve(options);\n  }\n\n  waitForDialogResolve = undefined;\n}));\n\nconst resolve = (result: any) => {\n  if (dialogResolve) {\n    dialogResolve(result);\n  }\n\n  dialogResolve = undefined;\n  dialogState = undefined;\n};\n\nexport const fakeAction = async (index: any) => {\n  const button = dialogState.buttons[index];\n  const action = button?.action;\n  let wasCalled = false;\n\n  if (action) {\n    await action(resolve, (newState: any) => {\n      wasCalled = true;\n      dialogState = newState;\n    });\n\n    if (!wasCalled) {\n      resolve(index);\n    }\n  } else {\n    resolve(index);\n  }\n};\n\nexport const getCurrentState = () => dialogState;\n\nexport const waitForDialog = async () => new Promise<any>(resolve => {\n  waitForDialogResolve = resolve;\n});\n"
  },
  {
    "path": "test/mocks/electron-store.ts",
    "content": "import sinon from 'sinon';\n\nconst mocks: Record<string, any> = {};\nconst store: Record<string, any> = {};\n\nconst getMock = sinon.fake(\n  (key: string, defaultValue: any) => mocks[key] ?? store[key] ?? defaultValue\n);\n\nconst setMock = sinon.fake(\n  (key: string, value: any) => {\n    store[key] = value;\n  }\n);\n\nconst deleteMock = sinon.fake(\n  (key: string) => {\n    delete store[key];\n  }\n);\n\nconst clearMock = sinon.fake(\n  () => {\n    for (const key of Object.keys(store)) {\n      delete store[key];\n    }\n  }\n);\n\nexport default class Store {\n  get = getMock;\n  set = setMock;\n  delete = deleteMock;\n  clear = clearMock;\n\n  get store() {\n    return {\n      ...store,\n      ...mocks\n    };\n  }\n\n  static mockGet = (key: string, result: any) => {\n    mocks[key] = result;\n  };\n\n  static clearMocks = () => {\n    for (const key of Object.keys(mocks)) {\n      delete mocks[key];\n    }\n  };\n\n  static mocks = {\n    get: getMock,\n    set: setMock,\n    delete: deleteMock,\n    clear: clearMock\n  };\n}\n"
  },
  {
    "path": "test/mocks/electron.ts",
    "content": "import sinon from 'sinon';\nimport tempy from 'tempy';\nimport path from 'path';\n\nconst temporaryDir = tempy.directory();\n\nprocess.env.TZ = 'America/New_York';\n(process.versions as any).chrome = '';\n\nexport const app = {\n  getPath: (name: string) => path.resolve(temporaryDir, name),\n  isPackaged: false,\n  getVersion: ''\n};\n\nexport const shell = {\n  showItemInFolder: sinon.fake()\n};\n\nexport const clipboard = {\n  writeText: sinon.fake()\n};\n\nexport const remote = {};\n"
  },
  {
    "path": "test/mocks/plugins.ts",
    "content": "import sinon from 'sinon';\nimport {Mutable, PartialDeep} from 'type-fest';\nimport type {Plugins} from '../../main/plugins';\n\nexport const plugins: PartialDeep<Mutable<Plugins>> = {\n  recordingPlugins: [],\n  sharePlugins: [],\n  editPlugins: []\n};\n"
  },
  {
    "path": "test/mocks/sentry.ts",
    "content": "import sinon from 'sinon';\n\nexport const isSentryEnabled = true;\n\nexport default {\n  captureException: sinon.fake()\n};\n"
  },
  {
    "path": "test/mocks/service-context.ts",
    "content": "\nexport class ShareServiceContext {}\nexport class RecordServiceContext {}\nexport class EditServiceContext {}\n"
  },
  {
    "path": "test/mocks/settings.ts",
    "content": "import sinon from 'sinon';\n\nconst mocks: Record<string, any> = {};\n\nconst mockGet = sinon.fake((key: string, defaultValue: any) => mocks[key] || defaultValue);\n\nexport const settings = {\n  get: mockGet,\n  set: sinon.fake(),\n  delete: sinon.fake(),\n  setMock: (key: string, value: any) => {\n    mocks[key] = value;\n  }\n};\n"
  },
  {
    "path": "test/mocks/video.ts",
    "content": "import sinon from 'sinon';\n\nconst mocks = {\n  open: sinon.fake(),\n  constructor: sinon.fake(),\n  getOrCreate: sinon.fake(() => new Video())\n};\n\nexport default class Video {\n  static getOrCreate = mocks.getOrCreate;\n  openEditorWindow = mocks.open;\n\n  constructor(...args: any[]) {\n    mocks.constructor(...args);\n  }\n\n  static mocks = mocks;\n}\n"
  },
  {
    "path": "test/mocks/window-manager.ts",
    "content": "import sinon from 'sinon';\nimport {SetOptional} from 'type-fest';\n\nimport type {WindowManager} from '../../main/windows/manager';\n\nimport * as dialogManager from './dialog';\n\nexport class MockWindowManager implements SetOptional<\nWindowManager,\n'setEditor' | 'setCropper' | 'setConfig' | 'setDialog' | 'setExports' | 'setPreferences'\n> {\n  editor = {\n    open: sinon.fake(),\n    areAnyBlocking: () => false\n  };\n\n  dialog = {\n    open: dialogManager.showDialog,\n    ...dialogManager\n  };\n}\n\nexport const windowManager = new MockWindowManager();\n"
  },
  {
    "path": "test/recording-history.ts",
    "content": "import {serial as testAny, TestInterface} from 'ava';\nimport tempy from 'tempy';\nimport fs from 'fs';\nimport sinon, {SinonFakeTimers} from 'sinon';\nimport path from 'path';\nimport type {MockWindowManager} from './mocks/window-manager';\n\nconst test = testAny as TestInterface<{\n  now: Date;\n  clock: SinonFakeTimers;\n  paths?: string[];\n}>;\n\nimport {mockImport, mockModule} from './helpers/mocks';\n\nmockImport('./windows/manager', 'window-manager');\nmockImport('./plugins', 'plugins');\nmockImport('./utils/sentry', 'sentry');\nmockImport('../common/analytics', 'analytics');\n\nimport {shell} from './mocks/electron';\nimport * as dialog from './mocks/dialog';\nimport {plugins} from './mocks/plugins';\nimport Sentry from './mocks/sentry';\nimport {windowManager} from './mocks/window-manager';\n\nimport {\n  recordingHistory,\n  getPastRecordings,\n  hasActiveRecording,\n  addRecording,\n  setCurrentRecording,\n  updatePluginState,\n  stopCurrentRecording,\n  cleanPastRecordings,\n  PastRecording\n} from '../main/recording-history';\nimport type {Video} from '../main/video';\n\nconst incomplete = path.resolve(__dirname, 'fixtures', 'incomplete.mp4');\nconst corrupt = path.resolve(__dirname, 'fixtures', 'corrupt.mp4');\n\ntest.before(t => {\n  t.context.now = new Date('2020-07-21T15:27:26.564Z');\n  t.context.clock = sinon.useFakeTimers(t.context.now.getTime());\n});\n\ntest.after(t => {\n  t.context.clock.restore();\n});\n\ntest.beforeEach(() => {\n  recordingHistory.clear();\n});\n\ntest.afterEach.always(t => {\n  if (t.context.paths) {\n    for (const path of t.context.paths) {\n      if (fs.existsSync(path)) {\n        fs.unlinkSync(path);\n      }\n    }\n  }\n});\n\ntest('`getPastRecordings()`', t => {\n  const existingPath = tempy.file({extension: 'mp4'});\n  const missingPath = tempy.file({extension: 'mp4'});\n\n  fs.writeFileSync(existingPath, 'data');\n  t.context.paths = [existingPath];\n\n  recordingHistory.set('recordings', [{filePath: existingPath}, {filePath: missingPath}]);\n\n  t.deepEqual(getPastRecordings(), [{filePath: existingPath} as PastRecording]);\n  t.deepEqual(recordingHistory.get('recordings'), [{filePath: existingPath} as PastRecording]);\n});\n\ntest('`hasActiveRecording()` with no recording', async t => {\n  t.false(await hasActiveRecording());\n});\n\ntest('`hasActiveRecording()` with playable recording', async t => {\n  const fakeService = {\n    title: 'Fake Service',\n    cleanUp: sinon.fake()\n  };\n\n  const fakePlugin = {\n    name: 'kap-fake-plugin',\n    recordServices: [fakeService]\n  };\n\n  plugins.recordingPlugins = [fakePlugin];\n\n  recordingHistory.set('activeRecording', {\n    filePath: incomplete,\n    name: 'Incomplete',\n    date: new Date().toISOString(),\n    apertureOptions: {},\n    plugins: {\n      'kap-fake-plugin': {\n        'Fake Service': {\n          some: 'state'\n        }\n      }\n    }\n  });\n\n  const checkPromise = hasActiveRecording();\n\n  const dialogState = await dialog.waitForDialog();\n  t.true(dialogState.detail.includes('playable'));\n\n  // Don't delete it until user is done interacting with the dialog\n  t.true(recordingHistory.has('activeRecording'));\n\n  await dialog.fakeAction(dialogState.buttons.findIndex((b: any) => b.label?.toLowerCase().includes('editor')));\n\n  const video = windowManager.editor.open.lastCall?.args?.[0] as Video;\n  t.deepEqual(video?.filePath, incomplete);\n  t.deepEqual(video?.title, 'Incomplete');\n\n  t.true(await checkPromise);\n\n  t.false(recordingHistory.has('activeRecording'));\n  t.true(fakeService.cleanUp.calledOnceWith({some: 'state'}));\n  t.deepEqual(\n    recordingHistory.get('recordings'),\n    [\n      {\n        filePath: incomplete,\n        name: 'Incomplete',\n        date: new Date().toISOString()\n      }\n    ]\n  );\n});\n\ntest('`hasActiveRecording()` with known corrupt recording', async t => {\n  recordingHistory.set('activeRecording', {\n    filePath: corrupt,\n    name: 'Corrupt',\n    date: new Date().toISOString(),\n    apertureOptions: {},\n    plugins: {}\n  });\n\n  const checkPromise = hasActiveRecording();\n\n  const dialogState = await dialog.waitForDialog();\n  t.true(dialogState.detail.includes('corrupt'));\n  t.true(dialogState.detail.includes('moov atom not found'));\n  t.truthy(dialogState.message);\n\n  // Don't delete it until user is done interacting with the dialog\n  t.true(recordingHistory.has('activeRecording'));\n\n  await dialog.fakeAction(dialogState.defaultId);\n\n  const newDialogState = await dialog.getCurrentState();\n  t.true(newDialogState.message.includes('unable'));\n\n  await dialog.fakeAction(newDialogState.defaultId);\n\n  t.true(shell.showItemInFolder.calledWithExactly(corrupt));\n\n  t.true(await checkPromise);\n\n  t.false(recordingHistory.has('activeRecording'));\n  t.is(recordingHistory.get('recordings').length, 0);\n});\n\ntest('`hasActiveRecording()` with unknown corrupt recording', async t => {\n  const filePath = tempy.file();\n  fs.writeFileSync(filePath, 'data');\n  t.context.paths = [filePath];\n\n  recordingHistory.set('activeRecording', {\n    filePath,\n    name: 'Bad',\n    date: new Date().toISOString(),\n    apertureOptions: {},\n    plugins: {}\n  });\n\n  const checkPromise = hasActiveRecording();\n\n  const dialogState = await dialog.waitForDialog();\n  t.true(dialogState.detail.includes('corrupt'));\n  t.false(dialogState.detail.includes('moov atom not found'));\n  t.falsy(dialogState.message);\n\n  // Don't delete it until user is done interacting with the dialog\n  t.true(recordingHistory.has('activeRecording'));\n\n  await dialog.fakeAction(dialogState.defaultId);\n\n  t.true(shell.showItemInFolder.calledWithExactly(filePath));\n\n  t.true(await checkPromise);\n\n  t.false(recordingHistory.has('activeRecording'));\n  t.is(recordingHistory.get('recordings').length, 0);\n\n  const sentryError = Sentry.captureException.lastCall.args[0];\n  t.true(sentryError.message.startsWith('Corrupt recording:'));\n});\n\ntest('`setCurrentRecording()`', t => {\n  setCurrentRecording({\n    filePath: 'some/path',\n    apertureOptions: {some: 'options'} as any,\n    plugins: {some: 'plugins'} as any\n  });\n\n  t.deepEqual(recordingHistory.get('activeRecording'), {\n    filePath: 'some/path',\n    name: 'Kapture 2020-07-21 at 11.27.26',\n    date: t.context.now.toISOString(),\n    apertureOptions: {some: 'options'} as any,\n    plugins: {some: 'plugins'} as any\n  });\n});\n\ntest('`updatePluginState()`', t => {\n  recordingHistory.set('activeRecording', {\n    name: 'Some name',\n    plugins: {\n      plugin1: {\n        service1: {some: 'state'}\n      },\n      plugin2: {\n        service2: {}\n      }\n    }\n  });\n\n  updatePluginState({\n    plugin1: {\n      service1: {some: 'state'}\n    },\n    plugin2: {\n      service2: {some: 'other state'}\n    }\n  });\n\n  t.deepEqual(recordingHistory.get('activeRecording.plugins'), {\n    plugin1: {\n      service1: {some: 'state'}\n    },\n    plugin2: {\n      service2: {some: 'other state'}\n    }\n  });\n});\n\ntest('`stopCurrentRecording()`', t => {\n  const filePath = tempy.file({extension: 'mp4'});\n  fs.writeFileSync(filePath, 'data');\n  t.context.paths = [filePath];\n\n  recordingHistory.set('activeRecording', {\n    filePath,\n    name: 'some name'\n  });\n\n  stopCurrentRecording();\n\n  t.false(recordingHistory.has('activeRecording'));\n  t.deepEqual(recordingHistory.get('recordings'), [{filePath, name: 'some name', date: t.context.now.toISOString()}]);\n\n  recordingHistory.set('activeRecording', {\n    filePath,\n    name: 'some name'\n  });\n\n  stopCurrentRecording('new name');\n\n  t.false(recordingHistory.has('activeRecording'));\n  t.deepEqual(recordingHistory.get('recordings'), [\n    {filePath, name: 'new name', date: t.context.now.toISOString()},\n    {filePath, name: 'some name', date: t.context.now.toISOString()}\n  ]);\n});\n\ntest('`cleanPastRecordings()`', t => {\n  const filePath = tempy.file({extension: 'mp4'});\n  fs.writeFileSync(filePath, 'data');\n  t.context.paths = [filePath];\n\n  recordingHistory.set('recordings', [\n    {filePath},\n    // Should ignore file that doesn't exist\n    {filePath: tempy.file({extension: 'mp4'})}\n  ]);\n\n  cleanPastRecordings();\n\n  t.false(fs.existsSync(filePath));\n});\n\ntest('`addRecording()`', t => {\n  const filePath = tempy.file({extension: 'mp4'});\n\n  addRecording({filePath} as PastRecording);\n\n  t.is(recordingHistory.get('recordings').length, 0);\n\n  fs.writeFileSync(filePath, 'data');\n  t.context.paths = [filePath];\n\n  addRecording({filePath} as PastRecording);\n\n  t.deepEqual(recordingHistory.get('recordings'), [{filePath} as PastRecording]);\n});\n"
  },
  {
    "path": "test/tsconfig.json",
    "content": "{\n  \"extends\": \"../tsconfig.json\",\n  \"compilerOptions\": {\n    \"noUnusedLocals\": false,\n    \"baseUrl\": \".\"\n  },\n  \"include\": [\n    \"**/*.ts\"\n  ]\n}\n"
  },
  {
    "path": "tsconfig.eslint.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"include\": [\n    \"main/**/*\",\n    \"main/**/*.js\",\n    \"test/**/*.ts\",\n    \"test/**/*.js\"\n  ]\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"extends\": \"@sindresorhus/tsconfig\",\n  \"compilerOptions\": {\n    \"outDir\": \"dist-js\",\n    \"target\": \"es2019\",\n    \"module\": \"commonjs\",\n    \"esModuleInterop\": true,\n    \"allowJs\": true,\n    \"sourceMap\": true,\n    \"inlineSources\": true,\n    \"lib\": [\n      \"esnext\"\n    ]\n  },\n  \"include\": [\n    \"node_modules/type-fest/index.d.ts\",\n    \"main/**/*\"\n  ],\n  \"exclude\": [\n    \"node_modules\"\n  ]\n}\n"
  }
]