New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

videojs-record

Package Overview
Dependencies
Maintainers
1
Versions
78
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

videojs-record - npm Package Compare versions

Comparing version 0.9.3 to 1.0.0

src/css/font/icons.json

12

bower.json
{
"name": "videojs-record",
"version": "0.9.3",
"version": "1.0.0",
"homepage": "https://github.com/collab-project/videojs-record",

@@ -32,5 +32,5 @@ "description": "A video.js plugin for recording audio/video/image files.",

"dependencies": {
"video.js": ">=4.11.0 < 5.0.0",
"video.js": ">=5.0.0",
"wavesurfer.js": ">=1.0.44",
"videojs-wavesurfer": ">=0.9.9 < 1.0.0",
"videojs-wavesurfer": ">=1.0.1",
"recordrtc": "git://github.com/muaz-khan/RecordRTC.git",

@@ -46,5 +46,9 @@ "libvorbis.js": ">=0.5.1"

"grunt-contrib-cssmin": ">=0.11.0",
"grunt-sass": "git://github.com/sindresorhus/grunt-sass.git",
"grunt-videojs-languages": ">=0.0.4",
"grunt-csscomb": "git://github.com/csscomb/grunt-csscomb.git"
"videojs-font": "git://github.com/videojs/font.git",
"lodash": "^3.9.3",
"material-design-icons": "^2.0.0",
"webfonts-generator": "git://github.com/sunflowerdeath/webfonts-generator.git"
}
}
'use strict';
var fs = require('fs');
var path = require('path');
var _ = require('lodash');
module.exports = function(grunt) {

@@ -58,9 +62,2 @@ var pkg, version, verParts;

},
csscomb: {
src: {
files: {
'src/css/videojs.record.css': ['src/css/videojs.record.css']
}
}
},
cssmin: {

@@ -77,2 +74,9 @@ target: {

},
sass: {
dist: {
files: {
'src/css/videojs.record.css': 'src/css/font/scss/videojs-icons.scss'
}
}
},
watch: {

@@ -103,5 +107,6 @@ gruntfile: {

grunt.loadNpmTasks('grunt-videojs-languages');
grunt.loadNpmTasks('grunt-csscomb');
grunt.loadNpmTasks('grunt-sass');
grunt.registerTask('pretask', ['jshint', 'csscomb', 'concat', 'vjslanguages']);
grunt.registerTask('font', ['generate-font', 'update-base64', 'sass']);
grunt.registerTask('pretask', ['jshint', 'concat', 'vjslanguages', 'font']);
grunt.registerTask('default', ['pretask', 'build', 'uglify']);

@@ -123,3 +128,3 @@

var ext = filename.substring(filename.lastIndexOf('.') + 1, filename.length);
if (["ttf", "svg", "eot", "woff"].indexOf(ext) > -1) {
if (['ttf', 'svg', 'eot', 'woff'].indexOf(ext) > -1) {
grunt.file.copy(absdir, 'dist/css/font/' + filename);

@@ -133,2 +138,73 @@ }

grunt.registerTask('generate-font', function() {
var done = this.async();
var webfontsGenerator = require('webfonts-generator');
var iconConfig = require('./src/css/font/icons.json');
var svgRootDir = iconConfig['root-dir'];
var icons = iconConfig.icons;
var iconFiles = icons.map(function(icon) {
// If root-dir is specified for a specific icon, use that.
if (icon['root-dir']) {
return icon['root-dir'] + icon.svg;
}
// Otherwise, use the default root-dir.
return svgRootDir + icon.svg;
});
webfontsGenerator({
files: iconFiles,
dest: 'src/css/font/',
fontName: iconConfig['font-name'],
cssDest: 'src/css/font/scss/_icons.scss',
cssTemplate: 'src/css/font/templates/scss.hbs',
htmlDest: 'src/css/font/preview.html',
htmlTemplate: 'src/css/font/templates/html.hbs',
html: true,
rename: function(iconPath) {
var fileName = path.basename(iconPath);
var iconName = _.result(_.find(icons, function(icon) {
var svgName = path.basename(icon.svg);
return svgName === fileName;
}), 'name');
return iconName;
},
types: ['svg', 'ttf', 'woff', 'eot']
}, function(error) {
if (error) {
console.error(error);
done(false);
}
done();
});
});
grunt.registerTask('update-base64', function() {
var iconScssFile = './src/css/font/scss/_icons.scss';
var fontFiles = {
ttf: './src/css/font/videojs-record.ttf',
woff: './src/css/font/videojs-record.woff'
};
var scssContents = fs.readFileSync(iconScssFile).toString();
Object.keys(fontFiles).forEach(function(font) {
var fontFile = fontFiles[font];
var fontContent = fs.readFileSync(fontFile);
var regex = new RegExp("(url.*font-" + font + ".*base64,)([^\\s]+)(\\).*)");
scssContents = scssContents.replace(regex, "$1" + fontContent.toString('base64') + "$3");
});
fs.writeFileSync(iconScssFile, scssContents);
});
};
{
"name": "videojs-record",
"version": "0.9.3",
"version": "1.0.0",
"description": "A video.js plugin for recording audio/video/image files.",

@@ -33,5 +33,5 @@ "main": "src/js/videojs.record.js",

"dependencies": {
"video.js": ">=4.11.0 < 5.0.0",
"video.js": ">=5.0.0",
"wavesurfer.js": ">=1.0.44",
"videojs-wavesurfer": ">=0.9.9 < 1.0.0",
"videojs-wavesurfer": ">=1.0.1",
"recordrtc": ">=5.0.8",

@@ -47,4 +47,8 @@ "libvorbis.js": ">=0.5.1"

"grunt-contrib-watch": ">=0.6.1",
"grunt-sass": "^1.0.0",
"grunt-videojs-languages": ">=0.0.4",
"grunt-csscomb": ">=3.0.0"
"videojs-font": ">=1.3.0",
"lodash": "^3.9.3",
"material-design-icons": "^2.0.0",
"webfonts-generator": "^0.2.4"
},

@@ -51,0 +55,0 @@ "author": "",

@@ -6,3 +6,3 @@ Video.js Record

![Screenshot](examples/img/screenshot.png?raw=true "Screenshot")
<img title="Screenshot" src="examples/img/screenshot.png?raw=true" width="329">

@@ -22,4 +22,6 @@ [![npm version](https://img.shields.io/npm/v/videojs-record.svg?style=flat)](https://www.npmjs.com/package/videojs-record)

Note about video.js v5.0: this plugin is [currently](https://github.com/collab-project/videojs-record/issues/6) only compatible with video.js v.4.x
releases (for example [4.12.15](https://github.com/videojs/video.js/tree/v4.12.15)).
Since v1.0 this plugin is compatible with video.js 5.0 and newer. If you want to use
this plugin with an older video.js 4.x version, check the
[archived releases](https://github.com/collab-project/videojs-record/releases)
for a 0.9.x or older release.

@@ -52,4 +54,4 @@ Dependencies

```html
<link href="//vjs.zencdn.net/4.12.15/video-js.css" rel="stylesheet">
<script src="//vjs.zencdn.net/4.12.15/video.js"></script>
<link href="//vjs.zencdn.net/5.0/video-js.css" rel="stylesheet">
<script src="//vjs.zencdn.net/5.0/video.js"></script>
```

@@ -223,13 +225,10 @@

To disable and hide specific controls, use the video.js `children`
To disable and hide specific controls, use the video.js `controlBar`
option:
```javascript
children: {
controlBar: {
children: {
// hide fullscreen control
fullscreenToggle: false
}
}
controlBar: {
// hide volume and fullscreen controls
volumeMenuButton: false,
fullscreenToggle: false
},

@@ -271,3 +270,3 @@ ```

<script src="videojs-record/dist/lang/nl.js"></script>
<script src="//vjs.zencdn.net/4.12.15/lang/nl.js"></script>
<script src="//vjs.zencdn.net/5.0/lang/nl.js"></script>
```

@@ -274,0 +273,0 @@

@@ -9,7 +9,7 @@ Font

To modify your generated font, use the `selection.js` file, located in the `css/font`
directory. You can import this file into the [IcoMoon](https://icomoon.io/app)
app. All the tags (class names) and the Unicode points of your glyphs are saved in
this file.
To modify the generated font, change the `icons.json` file, located in the `src/css/font`
directory. Then run `grunt` to update the generated font files and CSS definitions. View
`src/css/font/preview.html` in a browser to see the result.
See the documentation for more info on how to use this package: https://icomoon.io/#docs/font-face
See the videojs `font` project documentation for more info:
https://github.com/videojs/font/blob/master/README.md
(function(window, videojs) {
'use strict';
videojs.RecordBase = videojs.Component.extend(
var VjsComponent = videojs.getComponent('Component');
var VjsButton = videojs.getComponent('Button');
videojs.RecordBase = videojs.extend(VjsComponent,
{

@@ -32,9 +35,9 @@ // recorder modes

/** @constructor */
init: function(player, options, ready)
constructor: function(player, options)
{
videojs.Component.call(this, player, options, ready);
VjsComponent.call(this, player, options);
}
});
videojs.RecordRTCEngine = videojs.RecordBase.extend(
videojs.RecordRTCEngine = videojs.extend(videojs.RecordBase,
{

@@ -145,3 +148,3 @@ /**

videojs.LibVorbisEngine = videojs.RecordBase.extend(
videojs.LibVorbisEngine = videojs.extend(videojs.RecordBase,
{

@@ -257,3 +260,4 @@ /**

*/
videojs.Recorder = videojs.RecordBase.extend({
videojs.Recorder = videojs.extend(videojs.RecordBase,
{
/**

@@ -264,27 +268,26 @@ * The constructor function for the class.

* @param {Object} options Player options.
* @param {Function} ready Ready callback function.
*/
init: function(player, options, ready)
constructor: function(player, options)
{
// run base component initializing with new options.
videojs.Component.call(this, player, options, ready);
VjsComponent.call(this, player, options);
// record settings
this.recordImage = this.options().options.image;
this.recordAudio = this.options().options.audio;
this.recordVideo = this.options().options.video;
this.recordAnimation = this.options().options.animation;
this.maxLength = this.options().options.maxLength;
this.debug = this.options().options.debug;
this.recordImage = this.options_.options.image;
this.recordAudio = this.options_.options.audio;
this.recordVideo = this.options_.options.video;
this.recordAnimation = this.options_.options.animation;
this.maxLength = this.options_.options.maxLength;
this.debug = this.options_.options.debug;
// audio settings
this.audioEngine = this.options().options.audioEngine;
this.audioWorkerURL = this.options().options.audioWorkerURL;
this.audioModuleURL = this.options().options.audioModuleURL;
this.audioBufferSize = this.options().options.audioBufferSize;
this.audioSampleRate = this.options().options.audioSampleRate;
this.audioEngine = this.options_.options.audioEngine;
this.audioWorkerURL = this.options_.options.audioWorkerURL;
this.audioModuleURL = this.options_.options.audioModuleURL;
this.audioBufferSize = this.options_.options.audioBufferSize;
this.audioSampleRate = this.options_.options.audioSampleRate;
// animation settings
this.animationFrameRate = this.options().options.animationFrameRate;
this.animationQuality = this.options().options.animationQuality;
this.animationFrameRate = this.options_.options.animationFrameRate;
this.animationQuality = this.options_.options.animationQuality;

@@ -304,3 +307,31 @@ // recorder state

// tweak player UI
// wait until player ui is ready
this.player().one('ready', this.setupUI.bind(this));
},
/**
* Player UI is ready.
*/
setupUI: function()
{
// insert custom controls on left-side of controlbar
this.player().controlBar.addChild(this.player().cameraButton);
this.player().controlBar.el().insertBefore(
this.player().cameraButton.el(),
this.player().controlBar.el().firstChild);
this.player().controlBar.el().insertBefore(
this.player().recordToggle.el(),
this.player().controlBar.el().firstChild);
// get rid of unused controls
if (this.player().controlBar.remainingTimeDisplay !== undefined)
{
this.player().controlBar.remainingTimeDisplay.dispose();
}
if (this.player().controlBar.liveDisplay !== undefined)
{
this.player().controlBar.liveDisplay.dispose();
}
// tweak player UI based on type
switch (this.getRecordType())

@@ -310,3 +341,3 @@ {

// reference to videojs-wavesurfer plugin
this.surfer = player.waveform;
this.surfer = this.player().waveform;

@@ -326,3 +357,3 @@ // initially hide playhead (fixed in wavesurfer 1.0.25)

this.player().bigPlayButton.hide();
if (this.player().options().controls)
if (this.player().options_.controls)
{

@@ -341,7 +372,3 @@ // progress control isn't used by this plugin

this.player().controlBar.show();
this.player().controlBar.el().style.display = 'block';
// disable currentTimeDisplay's 'timeupdate' event listener that
// constantly tries to reset the current time value to 0
this.player().off('timeupdate');
this.player().controlBar.el().style.display = 'flex';
}

@@ -351,2 +378,6 @@ break;

// disable currentTimeDisplay's 'timeupdate' event listener that
// constantly tries to reset the current time value to 0
this.player().off('timeupdate');
// display max record time

@@ -491,5 +522,2 @@ this.setDuration(this.maxLength);

// hide live display indicator
this.player().controlBar.liveDisplay.hide();
// reset playback listeners

@@ -570,7 +598,12 @@ this.off(this.player(), 'timeupdate', this.playbackTimeUpdate);

{
// show live video preview
// show live preview
this.mediaElement = this.player().el().firstChild;
this.mediaElement.muted = true;
this.mediaElement.controls = false;
// mute incoming audio for feedback loops
this.mediaElement.muted = true;
// hide the volume bar while it's muted
this.displayVolumeControl(false);
// start stream

@@ -668,3 +701,3 @@ this.load(URL.createObjectURL(this.stream));

// notify UI
this.trigger('startRecord');
this.player().trigger('startRecord');
}

@@ -684,3 +717,3 @@ },

// notify UI
this.trigger('stopRecord');
this.player().trigger('stopRecord');

@@ -698,3 +731,3 @@ if (this.getRecordType() !== this.IMAGE_ONLY)

// notify listeners that image data is (already) available
this.trigger('finishRecord');
this.player().trigger('finishRecord');
}

@@ -713,3 +746,3 @@ }

// otherwise it'll break recording
this.one('finishRecord', this.stopStream);
this.player().one('finishRecord', this.stopStream);

@@ -805,3 +838,3 @@ // stop recording

// notify listeners that data is available
this.trigger('finishRecord');
this.player().trigger('finishRecord');

@@ -847,3 +880,3 @@ // Pausing the player so we can visualize the recorded data

// notify listeners that data is available
this.trigger('finishRecord');
this.player().trigger('finishRecord');

@@ -869,3 +902,4 @@ // remove previous listeners

// update time during playback
this.on(this.player(), 'timeupdate', this.playbackTimeUpdate);
this.on(this.player(), 'timeupdate',
this.playbackTimeUpdate);

@@ -876,8 +910,14 @@ // because there are 2 separate data streams for audio

// stream in the regular video.js player.
if (this.getRecordType() === this.AUDIO_VIDEO && this.isChrome())
if (this.getRecordType() === this.AUDIO_VIDEO &&
this.isChrome())
{
if (this.extraAudio === undefined)
{
this.extraAudio = this.player().createEl('audio');
this.extraAudio = this.createEl('audio');
this.extraAudio.id = 'extraAudio';
// handle volume changes in extra audio
// for chrome
this.player().on('volumechange',
this.onVolumeChange.bind(this));
}

@@ -889,3 +929,4 @@

// pause extra audio when player pauses
this.on(this.player(), 'pause', this.onPlayerPause);
this.on(this.player(), 'pause',
this.onPlayerPause);
}

@@ -900,2 +941,5 @@

this.mediaElement.muted = false;
// show the volume bar when it's unmuted
this.displayVolumeControl(true);
}

@@ -920,3 +964,3 @@

// notify listeners that data is available
this.trigger('finishRecord');
this.player().trigger('finishRecord');

@@ -951,2 +995,18 @@ // animation data is ready

/**
* Fired when the volume in the temporary audio element
* for Chrome in audio+video mode is present.
*/
onVolumeChange: function()
{
var volume = this.player().volume();
if (this.player().muted())
{
// muted volume
volume = 0;
}
this.extraAudio.volume = volume;
},
/**
* Invoked during recording and displays the remaining time.

@@ -1179,2 +1239,5 @@ */

// hide volume control to prevent feedback
this.displayVolumeControl(false);
// start/resume live preview

@@ -1262,2 +1325,21 @@ this.load(URL.createObjectURL(this.stream));

/**
* Show/hide the volume menu.
*/
displayVolumeControl: function(display)
{
if (this.player().controlBar.volumeMenuButton !== undefined)
{
if (display === true)
{
display = 'block';
}
else
{
display = 'none';
}
this.player().controlBar.volumeMenuButton.el().style.display = display;
}
},
/**
* Format seconds as a time string, H:MM:SS, M:SS or M:SS:MMM.

@@ -1335,13 +1417,10 @@ *

* Button to toggle between start and stop recording
* @param {videojs.Player|Object} player
* @param {Object=} options
* @class
* @constructor
*/
RecordToggle = videojs.Button.extend(
RecordToggle = videojs.extend(VjsButton,
{
/** @constructor */
init: function(player, options)
constructor: function(player, options)
{
videojs.Button.call(this, player, options);
VjsButton.call(this, player, options);

@@ -1372,4 +1451,4 @@ this.on('click', this.onClick);

// add the vjs-record-start class to the element so it can change appearance
this.removeClass('vjs-record-stop');
this.addClass('vjs-record-start');
this.removeClass('vjs-icon-record-start');
this.addClass('vjs-icon-record-stop');

@@ -1382,4 +1461,4 @@ // update label

// add the vjs-record-stop class to the element so it can change appearance
this.removeClass('vjs-record-start');
this.addClass('vjs-record-stop');
this.removeClass('vjs-icon-record-stop');
this.addClass('vjs-icon-record-start');

@@ -1392,13 +1471,10 @@ // update label

* Button to toggle between create and retry snapshot image
* @param {videojs.Player|Object} player
* @param {Object=} options
* @class
* @constructor
*/
CameraButton = videojs.Button.extend(
CameraButton = videojs.extend(VjsButton,
{
/** @constructor */
init: function(player, options)
constructor: function(player, options)
{
videojs.Button.call(this, player, options);
VjsButton.call(this, player, options);

@@ -1433,5 +1509,5 @@ this.on('click', this.onClick);

{
// add the vjs-record-start class to the element so it can change appearance
this.removeClass('vjs-record-stop');
this.addClass('vjs-record-start');
// add class to the element so it can change appearance
this.removeClass('vjs-icon-photo-camera');
this.addClass('vjs-icon-photo-retry');

@@ -1443,5 +1519,5 @@ // update label

{
// add the vjs-record-stop class to the element so it can change appearance
this.removeClass('vjs-record-start');
this.addClass('vjs-record-stop');
// add class to the element so it can change appearance
this.removeClass('vjs-icon-photo-retry');
this.addClass('vjs-icon-photo-camera');

@@ -1454,13 +1530,10 @@ // update label

* Button to select recording device
* @param {videojs.Player|Object} player
* @param {Object=} options
* @class
* @constructor
*/
DeviceButton = videojs.Button.extend(
DeviceButton = videojs.extend(VjsButton,
{
/** @constructor */
init: function(player, options)
constructor: function(player, options)
{
videojs.Button.call(this, player, options);
VjsButton.call(this, player, options);

@@ -1481,13 +1554,10 @@ this.on('click', this.onClick);

* Icon indicating recording is active.
* @param {videojs.Player|Object} player
* @param {Object=} options
* @class
* @constructor
*/
RecordIndicator = videojs.Component.extend(
RecordIndicator = videojs.extend(VjsComponent,
{
/** @constructor */
init: function(player, options)
constructor: function(player, options)
{
videojs.Component.call(this, player, options);
VjsComponent.call(this, player, options);

@@ -1507,17 +1577,11 @@ this.on(player, 'startRecord', this.show);

* Canvas for displaying snapshot image.
* @param {videojs.Player|Object} player
* @param {Object=} options
* @class
* @constructor
*/
RecordCanvas = videojs.Component.extend();
RecordCanvas = videojs.extend(VjsComponent);
/**
* Image for displaying animated GIF image.
* @param {videojs.Player|Object} player
* @param {Object=} options
* @class
* @constructor
*/
AnimationDisplay = videojs.Component.extend();
AnimationDisplay = videojs.extend(VjsComponent);

@@ -1529,8 +1593,10 @@ /**

*/
var createButton = function(className, label)
var createButton = function(className, label, iconName)
{
var props = {
className: 'vjs-' + className + '-button vjs-control',
className: 'vjs-' + className + '-button vjs-control vjs-icon-' + iconName,
innerHTML: '<div class="vjs-control-content"><span class="vjs-control-text">' +
label + '</span></div>',
};
var attrs = {
role: 'button',

@@ -1540,3 +1606,3 @@ 'aria-live': 'polite', // let the screen reader user know that the text of the button may change

};
return videojs.Component.prototype.createEl(null, props);
return VjsComponent.prototype.createEl('div', props, attrs);
};

@@ -1548,5 +1614,7 @@

className: 'vjs-record',
};
var attrs = {
tabIndex: 0
};
return videojs.Component.prototype.createEl(null, props);
return VjsComponent.prototype.createEl('div', props, attrs);
};

@@ -1611,3 +1679,3 @@

{
var settings = videojs.util.mergeOptions(defaults, options);
var settings = videojs.mergeOptions(defaults, options);
var player = this;

@@ -1621,3 +1689,3 @@

});
player.el().appendChild(player.recorder.el());
player.addChild(player.recorder);

@@ -1627,5 +1695,6 @@ // add device button

{
'el': createButton('device', player.localize('Device'))
'el': createButton('device', player.localize('Device'),
'device-perm')
});
player.recorder.el().appendChild(player.deviceButton.el());
player.recorder.addChild(player.deviceButton);

@@ -1635,3 +1704,3 @@ // add record indicator

{
'el': videojs.Component.prototype.createEl(null,
'el': VjsComponent.prototype.createEl('div',
{

@@ -1642,3 +1711,3 @@ className: 'vjs-record-indicator vjs-control'

player.recordIndicator.hide();
player.recorder.el().appendChild(player.recordIndicator.el());
player.recorder.addChild(player.recordIndicator);

@@ -1648,3 +1717,3 @@ // add canvas for recording and displaying image

{
'el': videojs.Component.prototype.createEl(null,
'el': VjsComponent.prototype.createEl('div',
{

@@ -1656,3 +1725,3 @@ className: 'vjs-record-canvas',

player.recordCanvas.hide();
player.recorder.el().appendChild(player.recordCanvas.el());
player.recorder.addChild(player.recordCanvas);

@@ -1662,3 +1731,3 @@ // add image for animation display

{
'el': videojs.Component.prototype.createEl(null,
'el': VjsComponent.prototype.createEl('div',
{

@@ -1670,3 +1739,3 @@ className: 'vjs-animation-display',

player.animationDisplay.hide();
player.recorder.el().appendChild(player.animationDisplay.el());
player.recorder.addChild(player.animationDisplay);

@@ -1676,7 +1745,6 @@ // add camera button

{
'el': createButton('camera', player.localize('Image'))
'el': createButton('camera', player.localize('Image'),
'photo-camera')
});
player.cameraButton.hide();
player.controlBar.el().insertBefore(player.cameraButton.el(),
player.controlBar.el().firstChild);

@@ -1686,7 +1754,6 @@ // add record toggle

{
'el': createButton('record', player.localize('Record'))
'el': createButton('record', player.localize('Record'),
'record-start')
});
player.recordToggle.hide();
player.controlBar.el().insertBefore(player.recordToggle.el(),
player.controlBar.el().firstChild);
};

@@ -1693,0 +1760,0 @@

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc