Compare commits

...
43 changed files with 13087 additions and 2926 deletions

29
.gitignore vendored
View file

@ -1,30 +1,5 @@
.DS_Store
/node_modules
/dist
/crate/target/*
/.log
/out
crash.log
# local env files
.env.local
.env.*.local
.env
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
#Electron-builder output
/dist_electron
*.sublime-workspace

View file

@ -1,63 +0,0 @@
stages:
- Build
- Upload
- Release
build:
stage: Build
tags:
# Use a macos runner to build.
- darwin
before_script:
- yarn
script:
- export VERSION=$(node -e "console.log(require('./package.json').version)")
- echo "VERSION=$VERSION" >> variables.env
- export APPNAME=$(node -e "console.log(require('./package.json').productName)")
- echo "APPNAME=$APPNAME" >> variables.env
- yarn build
artifacts:
reports:
dotenv: variables.env
name: $CI_COMMIT_REF_SLUG
paths:
- dist_electron/*.dmg
- dist_electron/*.zip
- dist_electron/*.yml
when: on_success
only:
- main
variables:
PACKAGE: '${APPNAME}-${VERSION}.dmg'
PACKAGE_REGISTRY_URL: '${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/${APPNAME}/${VERSION}'
# Upload package to gitlab registry.
upload:
stage: Upload
needs:
- job: build
artifacts: true
rules:
- if: $CI_COMMIT_TAG
when: never # Do not run this job when a tag is created manually
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH # Run this job when when commits are pushed to the default branch
script:
# Take the package we built and place it in the package registry.
- 'curl --header "JOB-TOKEN: $CI_JOB_TOKEN" --upload-file "dist_electron/${PACKAGE}" "${PACKAGE_REGISTRY_URL}/${PACKAGE}"'
auto-release-master:
image: registry.gitlab.com/gitlab-org/release-cli
needs:
- job: build
artifacts: true
- job: upload
artifacts: true
stage: Release
rules:
- if: $CI_COMMIT_TAG
when: never # Do not run this job when a tag is created manually
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH # Run this job when when commits are pushed to the default branch
script:
- echo "Release $VERSION"
- release-cli create --name "Release $VERSION" --tag-name v$VERSION --description "Release $CI_COMMIT_TITLE" --ref $CI_COMMIT_SHA --assets-link "{\"name\":\"${APPNAME}\",\"url\":\"${PACKAGE_REGISTRY_URL}/${PACKAGE}\"}"

2
.npmrc
View file

@ -1,2 +1,2 @@
@crimata:registry=https://gitlab.com/api/v4/projects/28849281/packages/npm/
//gitlab.com/api/v4/projects/28849281/packages/npm/:_authToken=${NPM_TOKEN}
//gitlab.com/api/v4/projects/28849281/packages/npm

View file

@ -1,29 +0,0 @@
# crimata-electron
## Project setup
```
yarn install
```
### Compiles and hot-reloads for development
```
yarn electron:serve
```
### Compiles and minifies for production
```
yarn electron:build
```
<!-- ### Run your unit tests
```
yarn test:unit
```
### Lints and fixes files
```
yarn lint
``` -->
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).

8
electron.sublime-project Normal file
View file

@ -0,0 +1,8 @@
{
"folders":
[
{
"path": "."
}
]
}

14
entitlements.plist Normal file
View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.debugger</key>
<true/>
<key>com.apple.security.device.audio-input</key>
<true/>
</dict>
</plist>
<!--<key>com.apple.security.cs.disable-library-validation</key>
<true/>-->

BIN
icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

11766
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,24 +1,54 @@
{
"name": "Crimata",
"productName": "crimata-messenger",
"name": "crimata",
"version": "1.0.0",
"private": true,
"description": "Cross-platform messenger app (Electron, Vue3, and TS)",
"author": {
"name": "Andrew Gundersen"
},
"description": "Cross-platform messenger app (Electron, Vue3)",
"main": "src/main.js",
"scripts": {
"start": "electron ."
"start": "electron-forge start",
"test": "echo \"Error: no test specified\" && exit 1",
"package": "electron-forge package",
"make": "electron-forge make"
},
"author": "Andrew Gundersen",
"license": "ISC",
"devDependencies": {
"@electron-forge/cli": "^6.0.0-beta.63",
"@electron-forge/maker-deb": "^6.0.0-beta.63",
"@electron-forge/maker-rpm": "^6.0.0-beta.63",
"@electron-forge/maker-squirrel": "^6.0.0-beta.63",
"@electron-forge/maker-zip": "^6.0.0-beta.63",
"electron": "^18.2.3"
},
"dependencies": {
"@crimata/nodeaudio": "^0.0.0",
"axios": "^0.21.1",
"base64-arraybuffer": "^1.0.1",
"electron-store": "^8.0.0",
"ws": "^7.3.1"
"ws": "^7.3.1",
"electron-squirrel-startup": "^1.0.0"
},
"devDependencies": {
"electron": "^13.2.3"
"config": {
"forge": {
"packagerConfig": {},
"makers": [
{
"name": "@electron-forge/maker-squirrel",
"config": {}
},
{
"name": "@electron-forge/maker-zip",
"platforms": [
"darwin"
]
},
{
"name": "@electron-forge/maker-deb",
"config": {}
},
{
"name": "@electron-forge/maker-rpm",
"config": {}
}
]
}
}
}

View file

@ -1,44 +1,43 @@
const { ipcMain } = require("electron");
const { post } = require("./api");
const store = require("./utils/store");
const { postLogin } = require("./api");
const { ipcEmit } = require("./utils/emitter");
const { launchSession, endSession } = require("./session");
const { backgroundMitt, ipcEmit } = require("./utils/emitter");
// store.set("account", "[email protected]");
store.set("account", "[email protected]");
store.set("account", process.env.ACCOUNT);
async function login(_e, email, password)
async function auth(_e, creds)
{
const res = await postLogin(email, password);
if (res)
const { error, data } = await post("/auth", creds);
if (error)
{
const account = res.crimataId;
store.set("account", account);
ipcEmit("set-account", account);
launchSession(account);
return true;
}
else
{
return false;
return data;
}
store.set("account", data);
ipcEmit("account", data);
launchSession(data);
};
function logout()
function logout(_e, reason)
{
store.delete("account");
ipcEmit("set-account", false);
ipcEmit("account", false, reason);
endSession();
};
function initAccount()
{
ipcMain.handle("login", login);
ipcMain.handle("auth", auth);
ipcMain.on("logout", logout);
const account = store.get("account");
if (account) launchSession(account);
}
backgroundMitt.on("logout", (reason) => logout(null, reason));
exports.initAccount = initAccount;

View file

@ -2,18 +2,37 @@ const axios = require('axios').default;
const config = require("./config");
const baseUrl = config.BUSINESS_URL + config.BUSINESS_PREFIX;
async function postLogin(email, password)
async function post(route, body)
{
try {
const res = await axios.post(baseUrl + "/account/login", {
email: email, password: password
});
return res.data;
} catch (e) {
return false;
let error;
let data;
try
{
const res = await axios.post(config.API + route, body);
data = res.data;
error = false;
}
catch (e)
{
if (e.response)
{
data = e.response.data;
}
else if (e.request)
{
data = "Can't connect to the server."
}
else
{
data = "Unknown error occured."
}
error = true;
}
return { error, data };
}
exports.postLogin = postLogin;
exports.post = post;

View file

@ -26,7 +26,9 @@ backgroundMitt.on("data", (int16Arr) => {
backgroundMitt.on("write", (int16Arr) => {
clearTimeout(setWriteId);
setWriteId = setTimeout(setPlaybackStatus.prototype.bind(null, false), 500);
setWriteId = setTimeout(() => {
setPlaybackStatus(false);
}, 500);
});
function setStreams()
@ -67,7 +69,7 @@ function stopRecording()
setRecordingStatus(false);
sendMessage({
category: "audio/L16",
category: "audio",
text: null,
blob: encode(nodeAudio.utils.mergeChunks(chunks).buffer)
});
@ -130,8 +132,3 @@ exports.initAudio = initAudio;
exports.terminateAudio = terminateAudio;
exports.playback = playback;
exports.streamState = streamState;
// setWriteId = setTimeout(() => {
// setPlaybackStatus(false);
// }, 500);

View file

@ -1,16 +1,11 @@
const { app } = require("electron");
const env = process.env;
const DOMAIN = "crimata.com";
const PLATFORM_PORT = env.PLATFORM_PORT || 8760;
const PLATFORM_IP = env.PLATFORM_IP || 'http://127.0.0.1';
const BUSINESS_PORT = env.BUSINESS_PORT || 3000;
const BUSINESS_IP = env.BUSINESS_IP || 'http://127.0.0.1';
/* If PROD env variable exists */
const prod = process.env.PROD;
module.exports = {
PLATFORM_URL: `${PLATFORM_IP}:${PLATFORM_PORT}`,
BUSINESS_URL: `${BUSINESS_IP}:${BUSINESS_PORT}`,
BUSINESS_PREFIX: '/api',
configPath: app.getPath('userData'),
}
PLATFORM: prod ? `https://app.${DOMAIN}` : `http://localhost:5000`,
API: prod ? `https://${DOMAIN}/api` : `http://localhost:5001`
}

View file

@ -14,7 +14,7 @@ function connectToPlatform(account)
{
if (pingId) clearInterval(pingId);
connection = new WebSocket(`${config.PLATFORM_URL}/${account}`)
connection = new WebSocket(`${config.PLATFORM}/${account}`)
.on("open", () => pingId = setInterval(() => connection.ping(null, true), 1000))
@ -24,11 +24,22 @@ function connectToPlatform(account)
.on("message", (payload) => backgroundMitt.emit("message", JSON.parse(payload)))
.on("close", (code, reason) => {
.on("close", (_code, reason) => {
setConnectionStatus(1);
if (reason !== "logout") {
reconnectId = setTimeout(() => connectToPlatform(account), 500);
if (reason)
{
if (reason == "unauthorized")
{
backgroundMitt.emit("logout", reason);
}
return;
}
reconnectId = setTimeout(() => connectToPlatform(account), 500);
});
}
@ -42,13 +53,13 @@ function sendMessage(message)
function disconnectFromPlatform()
{
clearTimeout(reconnectId);
connection.close(1000, "logout");
if (connection.open) connection.close(1000, "logout");
}
function setConnectionStatus(status)
{
updateTray("disconnect", status);
ipcEmit("ws-status", status);
ipcEmit("ws", status);
}
ipcMain.on("message", (_e, message) => sendMessage(message));
@ -56,25 +67,3 @@ ipcMain.on("message", (_e, message) => sendMessage(message));
exports.connectToPlatform = connectToPlatform;
exports.sendMessage = sendMessage;
exports.disconnectFromPlatform = disconnectFromPlatform;
/* ------------- Test Messages------------- */
setTimeout(() => {
// sendMessage({
// category: "text/plain",
// text: "show contacts",
// blob: null
// })
sendMessage({
category: "text/plain",
text: "add contact John Smith, [email protected]",
blob: null
})
}, 2000);

View file

@ -3,10 +3,9 @@ const { app, protocol, ipcMain, nativeTheme } = require("electron");
const store = require("./utils/store");
const { initTray } = require("./tray");
const { postLogin } = require("./api");
const { createWin } = require("./window");
const { initAccount } = require("./account");
const { terminateAudio } = require("./audio");
// const { terminateAudio } = require("./audio");
// Assert a light theme
nativeTheme.themeSource = "light";
@ -26,4 +25,4 @@ app.on("ready", () =>
app.on("activate", createWin);
// Prevents app from quitting on window close event
app.on('window-all-closed', (e) => e.preventDefault());
app.on('window-all-closed', (e) => e.preventDefault());

View file

@ -1,52 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
html, body {
margin: 0;
padding: 0;
font-family: "SF Pro Text";
font-size: 14px;
}
#titlebar {
position: fixed;
width: 100vw;
height: 54px;
opacity: 0;
-webkit-app-region: drag;
border: none;
outline: none;
z-index: 1;
}
.building {
width: 250px;
height: 150px;
}
.email {
padding: 15px;
}
.email-subject {
font-size: 16px;
margin-top: 10px;
}
</style>
</head>
<body>
<button id="titlebar"></button>
<div id="content"></div>
<script>
window.mainApi.on("preview-init", (content) => {
document.getElementById("content").innerHTML = content;
});
</script>
</body>
</html>

View file

@ -1,14 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24.87" height="23.001" viewBox="0 0 24.87 23.001">
<g id="Group_541" data-name="Group 541" transform="translate(-4127 -507)">
<g id="Group_33" data-name="Group 33" transform="translate(4127 507)">
<g id="Group_32" data-name="Group 32" transform="translate(0 0)">
<g id="Group_31" data-name="Group 31" transform="translate(0 11.948)">
<circle id="Ellipse_50" data-name="Ellipse 50" cx="5.527" cy="5.527" r="5.527" transform="translate(0 0)" fill="#383838"/>
<circle id="Ellipse_51" data-name="Ellipse 51" cx="5.527" cy="5.527" r="5.527" transform="translate(13.817 0)" fill="#383838"/>
</g>
<circle id="Ellipse_52" data-name="Ellipse 52" cx="5.527" cy="5.527" r="5.527" transform="translate(6.898 0)" fill="#383838"/>
<path id="Path_150" data-name="Path 150" d="M36.27,83.67" transform="translate(-30.733 -66.196)" fill="#ff0"/>
<svg id="logo" xmlns="http://www.w3.org/2000/svg" width="24.87" height="23.001" viewBox="0 0 24.87 23.001">
<g id="Group_33" data-name="Group 33">
<g id="Group_32" data-name="Group 32" transform="translate(0 0)">
<g id="Group_31" data-name="Group 31" transform="translate(0 11.948)">
<circle id="Ellipse_50" data-name="Ellipse 50" cx="5.527" cy="5.527" r="5.527" transform="translate(0 0)" fill="#383838"/>
<circle id="Ellipse_51" data-name="Ellipse 51" cx="5.527" cy="5.527" r="5.527" transform="translate(13.817 0)" fill="#383838"/>
</g>
<circle id="Ellipse_52" data-name="Ellipse 52" cx="5.527" cy="5.527" r="5.527" transform="translate(6.898)" fill="#383838"/>
<path id="Path_150" data-name="Path 150" d="M36.27,83.67" transform="translate(-30.733 -66.196)" fill="#ff0"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 973 B

After

Width:  |  Height:  |  Size: 845 B

Before After
Before After

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -1,6 +1,7 @@
import Splash from "./splash.js";
import Messenger from "./messenger.js";
import Login from "./login.js";
import Splash from "./splash.js";
import Header from "./header.js";
import Messenger from "./messenger.js";
const account = Vue.ref(null);
@ -8,6 +9,7 @@ const App =
{
components: {
Splash,
Header,
Messenger,
Login
},
@ -18,25 +20,25 @@ const App =
},
template: `
<main id="app" v-if="account !== null">
<div id="app" v-if="account !== null">
<button id="header-titlebar" />
<Header />
<Messenger v-if="account"/>
<Login v-else />
</main>
</div>
<Splash v-else />`
}
window.mainApi.on("set-account", (account_) => {
console.log("setting-account");
account.value = account_;
window.mainApi.on("account", (value, reason) => {
if (reason) alert(reason);
account.value = value;
});
export default App;
export { account };
// Is called when window is about to close or reload
window.onbeforeunload = () => console.log("beforeUnload");
window.onbeforeunload = () => console.log("beforeunload");

View file

@ -1,6 +1,6 @@
const Bubble =
{
props: ["category", "text", "blob", "modifier", "child"],
props: ["category", "text", "html", "blob", "modifier", "child"],
setup(props)
{
@ -9,28 +9,40 @@ const Bubble =
}
Vue.onMounted(() => {
document.getElementById("messenger").dispatchEvent(new CustomEvent('adjust-scroll'));
const el = document.getElementById("messenger");
setTimeout(() => {
el.dispatchEvent(new CustomEvent('adjust-scroll'));
}, 100);
});
if (!props.text) props.text = "...";
return { getUrl };
},
template: `
<div :class="'bubble ' + modifier + '-bubble ' + modifier +'-'+ child">
<p v-if="category=='text/plain'">{{ text }}</p>
<small class="bubble-notify" v-if="modifier=='ai'"></small>
<p v-else-if="category=='audio/L16'">{{ text }}</p>
<p v-if="category=='text'">{{ text }}</p>
<div v-else-if="category=='text/html'" v-html="text"></div>
<p v-else-if="category=='audio'">{{ text }}</p>
<img v-else-if="category.startsWith('image/')" :src="getUrl()" />
<div v-else-if="category=='html'" v-html="html"></div>
<a v-else :href="getUrl()">{{ text }}</a>
<img v-else-if="category == 'image'" :src="getUrl()" />
<a v-else :download="text" :href="getUrl()">{{ text }}</a>
</div>`
};
export default Bubble;
// TODO: should include other styling/actions for audio bubbles
// TODO: should include other styling/actions for audio bubbles

View file

@ -20,15 +20,10 @@ const Context =
template: `
<span :class="'context ' + modifier + '-context'">
<div
v-show="avatar"
class="context-avatar"
<img v-show="avatar" class="context-avatar"
:class="{ audio: playback }"
>
<img :src="'data:image/png;base64,' + avatar" />
</div>
:src="'data:image/png;base64,' + avatar"
/>
{{ context }}

View file

@ -1,5 +1,5 @@
const saveLocation = "input_item_position";
const defaultPosition = { x: 15, y: window.innerHeight - 300 };
const defaultPosition = { x: 15, y: 400 };
//---Dragabble Helper Funcs--------------------------------------
@ -33,9 +33,10 @@ function calcPosition (elementPosition, elementLength, percent, short, win) {
return elementPosition
}
function draggify(elementId, margin) {
function draggify(elementId, parentId, margin) {
let element;
let parent;
/* only compatible with elements having equal width and height */
let elementLength;
@ -52,23 +53,20 @@ function draggify(elementId, margin) {
let xShort;
let yShort;
// Keep track of window size;
let winW = window.innerWidth;
let winH = window.innerHeight;
//---Reposition Anime-----------------------------------------------
// Move element to target smoothly.
const repositionAnime = (xChange, yChange) => {
const xStep = xChange / 6000;
const yStep = yChange / 6000;
for (let i = 1; i <= 6000; i++) {
const xStep = xChange / 1000;
const yStep = yChange / 1000;
for (let i = 1; i <= 1000; i++) {
setTimeout(() => {
elementX.value += xStep;
elementY.value += yStep;
}, 16) // 60fps
}, 30) // 60fps
}
}
@ -79,9 +77,8 @@ function draggify(elementId, margin) {
const onMouseMove = (e) => {
e.preventDefault();
const inputItemRect = element.getBoundingClientRect();
elementX.value = inputItemRect.left + e.movementX;
elementY.value = inputItemRect.top + e.movementY;
elementX.value = element.offsetLeft + e.movementX;
elementY.value = element.offsetTop + e.movementY;
}
// Add an event listener for dragging.
@ -102,6 +99,9 @@ function draggify(elementId, margin) {
let x = 0; // vector change
let y = 0;
const winW = parent.clientWidth;
const winH = parent.clientHeight;
if (elementX.value < 0) {
x = (elementX.value - margin)*-1
}
@ -138,12 +138,8 @@ function draggify(elementId, margin) {
// Update position of targetEl on windowResize.
const onWindowResize = (_e) => {
// Update window dimensions.
winW = window.innerWidth;
winH = window.innerHeight;
elementX.value = calcPosition(elementX.value, elementLength, percentX, xShort, winW);
elementY.value = calcPosition(elementY.value, elementLength, percentY, yShort, winH);
elementX.value = calcPosition(elementX.value, elementLength, percentX, xShort, parent.clientWidth);
elementY.value = calcPosition(elementY.value, elementLength, percentY, yShort, parent.clientHeight);
savePosition();
}
@ -160,27 +156,28 @@ function draggify(elementId, margin) {
Vue.onMounted(() => {
element = document.getElementById(elementId);
parent = document.getElementById(parentId);
elementLength = element.offsetWidth;
// Initialize the positional references.
percentX = elementX.value / window.innerWidth;
percentY = elementY.value / window.innerHeight;
percentX = elementX.value / parent.clientWidth;
percentY = elementY.value / parent.clientHeight;
xShort = calcSideProximity(elementX.value, elementLength, window.innerWidth);
yShort = calcSideProximity(elementX.value, elementLength, window.innerHeight);
xShort = calcSideProximity(elementX.value, elementLength, parent.clientWidth);
yShort = calcSideProximity(elementX.value, elementLength, parent.clientHeight);
// Then, we can listen for window resize (and mousedown).
element.addEventListener("mousedown", onMouseDown);
window.addEventListener('resize', onWindowResize)
parent.addEventListener('resize', onWindowResize)
});
// remove event listeners on component dismount.
Vue.onUnmounted(() => {
element.removeEventListener('mousedown', onMouseDown);
window.removeEventListener('resize', onWindowResize)
window.removeEventListener('mouseup', onMouseUp)
window.removeEventListener('mousemove', onMouseMove);
parent.removeEventListener('resize', onWindowResize)
parent.removeEventListener('mouseup', onMouseUp)
parent.removeEventListener('mousemove', onMouseMove);
});
// Try loading initPosition, otherwise set default values

View file

@ -0,0 +1,24 @@
const onDrop = async (e) => {
const file = e.dataTransfer.items[0].getAsFile();
if (file.size >= 1 * 1000 * 1000) {
alert("File must be under 1MB.");
return;
}
const buffer = await file.arrayBuffer();
const b64String = await window.mainApi.invoke("encode", buffer);
let category = "file";
if (file.type.startsWith("image/")) {
category = "image"
}
window.mainApi.send("message", {
category: category,
text: file.name,
blob: b64String
});
}
export default onDrop;

View file

@ -1,24 +1,20 @@
let el;
const initScroll = (elementId) => {
el = document.getElementById(elementId);
el.addEventListener('adjust-scroll', adjustScroll);
window.addEventListener('resize', adjustScroll);
}
const isBottom = () => {
if (el) {
return el.scrollHeight - el.clientHeight <= el.scrollTop + 1;
}
}
const adjustScroll = () => {
if (el) {
const scroll = () => {
if (!el) {
el = document.getElementById("messenger");
window.addEventListener('resize', scroll);
}
el.scrollTo({
top: el.scrollHeight - el.clientHeight,
behavior: 'smooth'
});
}
}
export default initScroll;
export default scroll;
// const isBottom = () => {
// if (el) {
// return el.scrollHeight - el.clientHeight <= el.scrollTop + 1;
// }
// }

View file

@ -17,25 +17,27 @@ const metaKeys = [
"Escape"
];
function useText(elementId, left, typing)
function useText(elementId, parentId, left)
{
let el;
let parent;
const show = Vue.ref(false);
const leftSide = Vue.ref(false);
const calcSide = () => {
window.innerWidth - left.value < inputLength ? leftSide.value = true : leftSide.value = false;
parent.clientWidth - left.value < inputLength ? leftSide.value = true : leftSide.value = false;
}
/* handle user typing */
Vue.onMounted(() => {
el = document.getElementById(elementId);
parent = document.getElementById(parentId);
window.addEventListener("keydown", (e) => {
if (!show.value && !metaKeys.includes(e.key))
if (!show.value && !metaKeys.includes(e.key) && !e.ctrlKey)
{
show.value = true;
}
@ -46,15 +48,23 @@ function useText(elementId, left, typing)
if (e.key === "Enter" && el.value)
{
if (el.value.length >= 250) {
alert("250 character limit")
return;
}
window.mainApi.send("message", {
category: "text/plain",
category: "text",
text: el.value,
blob: null
});
show.value = false;
}
});
calcSide();
});
Vue.watch(left, calcSide);
@ -68,8 +78,6 @@ function useText(elementId, left, typing)
}
});
calcSide();
return { leftSide, show };
}

View file

@ -0,0 +1,23 @@
export const isEmail = (value) => {
console.log(value);
if (!/(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/.test(value)) {
return "Not a valid email."
}
}
export const isPassword = (value) => {
if (!/^(?=.*[0-9])(?=.*[!@#$%^&*])[a-zA-Z0-9!@#$%^&*]{6,16}$/.test(value)) {
return "Password must have symbol, number, uppercase and be 6-16 length.";
}
}
/**
* Name is optional and therefore doesn't return error if no value.
*/
export const isName = (value) => {
if (value) {
if (!/^([a-zA-Z ]){5,30}$/.test(value)) {
return "Name must be at least 5 long.";
}
}
}

View file

@ -0,0 +1,86 @@
// Basic debounce implentation
const debounce = (fn, delay) => {
let timeoutId;
return (...args) => {
if (timeoutId) clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
fn(...args);
}, delay);
}
}
/**
* Dynamic form component with auto submission and validation.
*
* props
* .fields => List of field objects
* .channel => e.g. "/login"
* .modifier => Form description
*/
const SmartForm =
{
props: ["fields", "channel", "modifier"],
setup(props)
{
// Error log below form
const log = Vue.ref(props.modifier);
/**
* Valid and submit form data to channel.
*/
const submit = async () => {
// Validate each field
for (let field of props.fields) {
const error = field.valid(field.value);
if (error) return error;
}
// Reduce fields into the form
const form = props.fields.reduce((v, n) => {
v[n.name] = n.value;
return v;
}, {});
console.log("Submitting =>", form);
return await window.mainApi.invoke(props.channel, form);
}
/**
* Wrapper for submit that is debounced and updates UI elements
*/
const preSub = debounce(async () => {
const error = await submit();
log.value = error ? error : props.modifier
}, 2000);
return { preSub, log };
},
template: `
<form id="smart-form" class="smart-form" v-on:input="preSub">
<div v-for="field in fields" style="display:flex">
<input type="text" class="text-field"
v-model="field.value"
:name="field.name"
:placeholder="field.placeholder"
/>
</div>
<div class="smart-form-logs">{{ log }}</div>
</form>`
}
export default SmartForm;

View file

@ -1,7 +1,7 @@
const Header =
{
setup() {
const onNavBar = (command) => window.mainApi.send('nav-bar', command);
const onNavBar = (command) => window.mainApi.send("nav", command);
return { onNavBar };
},

View file

@ -11,9 +11,9 @@ const Input =
const recording = Vue.ref(false);
const { elementX, elementY } = draggify("input", 15);
const { elementX, elementY } = draggify("input", "app", 15);
const { leftSide, show } = useText("input-text", elementX);
const { leftSide, show } = useText("input-text", "app", elementX);
window.mainApi.on("record", (status) => recording.value = status);

View file

@ -1,58 +1,44 @@
import SmartForm from "./form.js";
import { isEmail, isPassword, isName } from "./control/valid.js";
const Login =
{
components: {
SmartForm
},
setup()
{
const usr = Vue.ref("");
const pwd = Vue.ref("");
const submitForm = async () => {
const success = await window.mainApi.invoke("login", usr.value, pwd.value);
if (!success) shake("login-form");
}
return { usr, pwd, submitForm };
return { isEmail, isPassword, isName };
},
template: `
<form id="login" @submit.prevent="submitForm">
<div id="login">
<!-- username and password forms -->
<div id="login-form">
<!-- username -->
<div class="login-input-box">
<input class="login-input" type="text" v-model="usr" />
<div class="login-input-modifier">Email</div>
</div>
<SmartForm
:fields="[{
name: 'email',
value: null,
placeholder: 'Email',
valid: isEmail
},
{
name: 'password',
value: null,
placeholder: 'Password',
valid: isPassword
},
{
name: 'name',
value: null,
placeholder: 'Name',
valid: isName
}]"
:channel="'auth'"
:modifier="'Login or enter name to register.'"
/>
<!-- password -->
<div class="login-input-box">
<input class="login-input" type="password" v-model="pwd" />
<div class="login-input-modifier">Password</div>
</div>
</div>
<!-- submit button; position: fixed -->
<button
class="login-submit-button button"
type="submit"
>
Login
</button>
</form> `
</div>`
}
/** Shake a div to indicate incorrect credentials */
const shake = (elementId) =>
{
const el = document.getElementById(elementId);
if (el)
el.classList.remove("shake");
setTimeout(() => {
if (el) el.classList.add("shake");
}, 250);
}
export default Login;
export default Login;

View file

@ -8,7 +8,7 @@ const Message =
Context
},
props: ["modifier", "content", "context", "avatar", "id"],
props: ["modifier", "content", "context", "avatar", "id", "seen"],
setup(props)
{
@ -18,10 +18,12 @@ const Message =
template: `
<div :id="id" :class="'message ' + modifier + '-message'">
<Bubble
<Bubble
v-for="(c, index) in content"
:id="'bubble-' + id + '-' + index"
:category="c.category"
:text="c.text"
:html="c.html"
:blob="c.blob"
:modifier="modifier"
:child="calcChild(index, content.length)"

View file

@ -1,4 +1,5 @@
import initScroll from "./control/scroll.js";
import onDrop from "./control/drop.js";
import scroll from "./control/scroll.js";
import WS from "./ws.js";
import Input from "./input.js";
@ -17,64 +18,49 @@ const Messenger = {
setup()
{
const person = Vue.ref("");
const messages = Vue.ref([{
modifier: "ai",
content: [{
category: "text/html",
text: `<div class="mail">
<h4>About HTML Parser</h4>
<p>Return the text of the most recently opened start tag. This should not normally be needed for structured processing, but may be useful in dealing with HTML “as deployed” or for re-generating input with minimal changes (whitespace between attributes can be preserved, etc.).</p>
</div>`,
blob: null
}],
context: "Email from David Spade",
avatar: null,
id: 0
}]);
const messages = Vue.ref([]);
const onDrop = async (e) => {
const file = e.dataTransfer.items[0].getAsFile();
const buffer = await file.arrayBuffer();
const b64String = await window.mainApi.invoke("encode", buffer);
window.mainApi.send("message", {
category: file.type,
text: file.name,
blob: b64String
});
}
let current = Vue.ref();
Vue.onMounted(() => {
window.mainApi.on("message", (message) => {
if (message.person)
window.mainApi.on("message", (update) => {
if (update.person)
{
person.value = message.person;
messages.value = message.messages;
person.value = update.person;
messages.value = update.messages;
current.value = messages.value.at(-1);
}
else if (message.name)
else if (update.name)
{
person.value = message;
person.value = update.name;
}
else if (message.content)
else if (update.modifier)
{
messages.value.push(message);
messages.value.push(update);
current.value = update;
}
else if (update.category)
{
current.value.content.push(update);
}
else if (update.context)
{
current.value.context = update.context;
}
else
{
messages.value[messages.value.length - 1].content.push(message);
current.value.content.at(-1).text = update.text;
}
setTimeout(scroll, 10);
});
initScroll("messenger");
window.mainApi.send("messenger");
});
Vue.onUnmounted(() => {
messages.value = [];
});
return { person, messages, onDrop };
},
@ -88,15 +74,13 @@ const Messenger = {
@dragover.prevent
@dragenter.prevent
>
<Message
v-for="message in messages"
:modifier="message.modifier"
:content="message.content"
:context="message.context"
:avatar="message.avatar"
:id="message.id"
<Message
v-for="message in messages"
v-bind="message"
/>
</div> `
</div>`
}
export default Messenger;

View file

@ -4,25 +4,37 @@ const Settings =
setup() {
const active = Vue.ref(false);
window.addEventListener("keydown", (e) => {
console.log("toggle settings");
})
function hideSettings(e) {
if (e.key == "Escape") {
active.value = false;
window.removeEventListener("keydown", hideSettings);
}
}
function showSettings() {
active.value = true;
window.addEventListener("keydown", hideSettings);
}
const logout = async () => {
await window.mainApi.send("logout");
}
return { active, logout };
return { showSettings, active, logout, window };
},
template: `
<div v-if="active" id="settings" >
<div v-if="active" id="settings">
<button class="settings-logout-button button" @click="logout">
Logout
</button>
</div>`
</div>
<button v-else class="settings-icon" @click="showSettings">
<img :src="window.path + 'assets/settingsIcon.svg'">
</button>`
}
export default Settings;

View file

@ -7,19 +7,19 @@ const WS =
Vue.onMounted(() => {
window.mainApi.on("ws-status", (val) => {
window.mainApi.on("ws", (val) => {
status.value = val;
});
});
return { status }
return { status, window }
},
template: `
<div id="ws">
<img id="ws-logo" src="./assets/connectLogo.svg"
<img id="ws-logo" :src="window.path + 'assets/connectLogo.svg'"
:class="{ move: status }"
>

View file

@ -1,2 +1,5 @@
import App from "./components/app.js";
window.path = "./";
Vue.createApp(App).mount("#app");

View file

@ -1,29 +1,48 @@
@font-face {
font-family: "Default";
src: url("assets/fonts/SF-Pro-Text-Regular.otf");
}
@font-face {
font-family: "Compact";
src: url("assets/fonts/SF-Compact-Display-Bold.otf");
}
@font-face {
font-family: "Rounded";
src: url("assets/fonts/SF-Compact-Rounded-Bold.otf");
}
html, body {
margin: 0;
padding: 0;
/*background-color: rgba(235, 235, 235, 0.75);*/
}
/* The first word of the class is the component that it modifys. */
#splash {
width: 100vw;
height: 100vh;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
#app {
font-family: "SF Pro Text";
position: relative; /* must explicitly be declared */
font-family: "Default";
-webkit-font-smoothing: antialiased;
height: 100vh;
width: 100vw;
height: 100vh; /* 100% for website */
width: 100vw; /* 100% for website */
/* Does not work on Linux */
border-radius: 15px;
}
#header-titlebar {
position: fixed;
width: 100vw;
position: absolute;
width: 100%;
height: 54px;
opacity: 0.75;
background-color: #EBEBEB;
@ -31,6 +50,8 @@ html, body {
border: none;
outline: none;
z-index: 1;
border-top-left-radius: 15px;
border-top-right-radius: 15px;
}
.contacts {
@ -46,6 +67,7 @@ html, body {
}
.contacts img {
height: 30px;
border-radius: 50%;
}
@ -59,38 +81,26 @@ html, body {
}
.contacts .email {
font-family: SF Compact Display;
font-weight: bold;
font-family: "Compact";
font-size: 12px;
color: #898989;
}
.mail {
padding: 12px;
margin: 0px;
background-color: #D6F2FF;
border-radius: inherit;
}
.mail h4 {
margin-top: 0px;
}
.mail p {
margin-bottom: 0px;
overflow: w;
word-wrap: break-word;
}
#header-menu {
position: fixed;
position: absolute;
margin-left: 20px;
margin-top: 20px;
z-index: 2;
/* Needed for Linux even with higher Z index */
-webkit-app-region: no-drag;
}
.header-menu-button {
min-width: 14px;
min-height: 14px;
border-radius: 7px;
border-radius: 50%;
}
.header-exit-button {
@ -119,87 +129,9 @@ html, body {
justify-content: center;
}
#login-form {
font-family: "SF Compact Display";
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding-bottom: 20px;
}
.login-input-box {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin-bottom: 10px;
}
.login-input {
background-color: #B9B9B9;
border: none;
outline: none;
padding: 16px;
text-decoration: none;
margin: 4px 2px;
cursor: pointer;
border-radius: 10px;
}
.login-input-modifier {
min-width: 181px;
font-size: 12px;
font-weight: bold;
text-align: left;
margin-left: 15px;
}
.login-submit-button {
font-family: "SF Compact Display";
position: fixed;
margin-top: 260px;
background-color: #58C4FD;
color: white;
padding: 10px 30px;
border-radius: 20px;
font-size: 14px;
font-weight: bold;
}
.login-submit-button:active {
background-color: #4296C3;
}
/* generic shaking functionality */
.shake {
animation: shake 0.82s cubic-bezier(.36,.07,.19,.97) both;
transform: translate3d(0, 0, 0);
backface-visibility: hidden;
perspective: 1000px;
}
@keyframes shake {
10%, 90% {
transform: translate3d(-1px, 0, 0);
}
20%, 80% {
transform: translate3d(2px, 0, 0);
}
30%, 50%, 70% {
transform: translate3d(-4px, 0, 0);
}
40%, 60% {
transform: translate3d(4px, 0, 0);
}
}
#messenger {
width: 100vw;
height: 100vh;
width: 100%;
height: 100%;
overflow: auto;
}
@ -209,8 +141,8 @@ html, body {
}
#ws {
position: fixed;
width: 100vw;
position: absolute;
width: 100%;
height: 54px;
display: flex;
align-items: center;
@ -251,6 +183,7 @@ html, body {
content: '';
display: inline-block;
position: absolute;
box-shadow: 0px 0px 10px 0px rgba(0,0,0,0.15);
}
.ws-dot::before {
@ -297,7 +230,6 @@ html, body {
border-radius: 50%;
/* Set opacity here to not affect child. */
background-color: rgba(235, 235, 235, 0.75);
cursor: pointer;
box-shadow: 0px 0px 10px 0px rgba(0,0,0,0.15);
}
@ -309,6 +241,7 @@ html, body {
padding: 10px;
outline: none;
border: none;
font-size: 14px;
pointer-events: none;
background-color: white;
z-index: -1;
@ -328,9 +261,9 @@ html, body {
}
#settings {
position: fixed;
width: 100vw;
height: 100vh;
position: absolute;
width: 100%;
height: 100%;
background-color: rgba(235, 235, 235, 0.75);
z-index: 4;
display: flex;
@ -339,6 +272,7 @@ html, body {
align-items: center;
animation-name: settings-appear;
animation-duration: 0.5s;
border-radius: inherit;
}
@keyframes settings-appear {
@ -351,7 +285,7 @@ html, body {
}
.settings-icon {
position: fixed;
position: absolute;
right: 0;
border: none;
outline: none;
@ -362,10 +296,8 @@ html, body {
margin-top: 19px;
z-index: 2;
background-color: Transparent;
}
.settings-icon:hover {
cursor: pointer;
/* Needed for Linux */
-webkit-app-region: no-drag;
}
.settings-account {
@ -375,16 +307,11 @@ html, body {
}
.settings-logout-button {
font-family: "SF Compact Display";
font-family: "Compact";
background-color: #B7B7B7;
padding: 10px 30px;
padding: 10px 20px;
border-radius: 20px;
font-size: 14px;
font-weight: bold;
}
.settings-logout-button:hover {
cursor: pointer;
}
.settings-version {
@ -398,7 +325,7 @@ html, body {
}
.message {
width: 100vw;
width: inherit;
display: flex;
flex-direction: column;
padding-top: 9px;
@ -423,6 +350,17 @@ html, body {
margin-bottom: 6px;
}
.message-session {
width: 100vw;
display: flex;
justify-content: center;
align-items: center;
font-family: "Compact";
font-size: 12px;
color: #9B9B9B;
margin-bottom: 18px;
}
.client-message {
align-items: flex-end;
}
@ -431,13 +369,40 @@ html, body {
align-items: flex-start;
}
.admin-message {
align-items: center;
}
.bubble {
position: relative;
max-width: 66vw;
font-family: "SF Pro Text";
max-width: 66%;
font-size: 14px;
border-radius: 18px;
margin-bottom: 4px;
overflow-wrap: break-word;
}
.bubble-notify {
position: absolute;
width: 12px;
height: 12px;
border-radius: 50%;
background-color: #58D9FF;
top: -5px;
left: -5px;
border: 2px solid #EBEBEB;
transform: scale(0);
animation-name: bubble-notify-anim;
animation-duration: 5s;
}
@keyframes bubble-notify-anim {
0%, 90% {
transform: scale(1);
}
100% {
transform: scale(0);
}
}
.ai-bubble {
@ -489,17 +454,17 @@ html, body {
.bubble > img {
border-radius: inherit;
display: block;
max-width: inherit;
max-width: 100%;
}
.bubble a {
display: inline-block;
font-family: "Rounded";
text-decoration: none;
border-radius: inherit;
font-family: "SF Compact Rounded";
background-color: #D9D9D9;
font-weight: bold;
padding: 12px;
padding: 10px;
color: #727272;
margin: 0px;
}
.context {
@ -507,9 +472,8 @@ html, body {
min-height: 14px;
display: flex;
align-items: center;
font-family: "SF Compact Display";
font-family: "Compact";
font-size: 12px;
font-weight: bold;
margin-top: 5px;
}
@ -522,18 +486,9 @@ html, body {
}
.context-avatar {
display: flex;
justify-content: center;
align-items: center;
width: 30px;
height: 30px;
background-color: white;
border-radius: 15px;
margin-right: 5px;
}
.context-avatar img {
border-radius: 50%;
height: 30px;
}
/* ---------- shared between components ---------- */
@ -548,6 +503,32 @@ html, body {
cursor: pointer;
}*/
/* shake a div to signal error*/
.shake {
animation: shake 0.82s cubic-bezier(.36,.07,.19,.97) both;
transform: translate3d(0, 0, 0);
backface-visibility: hidden;
perspective: 1000px;
}
@keyframes shake {
10%, 90% {
transform: translate3d(-1px, 0, 0);
}
20%, 80% {
transform: translate3d(2px, 0, 0);
}
30%, 50%, 70% {
transform: translate3d(-4px, 0, 0);
}
40%, 60% {
transform: translate3d(4px, 0, 0);
}
}
.audio {
animation-name: audio-anim;
animation-duration: 2s;
@ -569,3 +550,29 @@ html, body {
box-shadow: 0 0 0 0.25em rgba(195, 195, 195, 0.25), 0 0 0 0.55em rgba(195, 195, 195, 0.45);
}
}
.smart-form {
width: 225px;
display: flex;
flex-direction: column;
justify-content: flex-start;
}
.text-field {
width: 100%;
padding: 16px;
background: none;
border: none;
margin: 4px;
font-size: 14px;
border-radius: 10px;
background-color: #d1d1d1;
outline: none;
}
.smart-form-logs {
font-family: "Compact";
font-size: 12px;
text-align: left;
margin-left: 10px;
}

View file

@ -3,20 +3,20 @@ const { contextBridge, ipcRenderer } = require("electron");
/** The main <-> render interface */
const validFromMainChannels = [
"set-account",
"message",
"ws-status",
"playback",
"account",
"message",
"record",
"playback"
"ws"
];
const validToMain = [
"messenger",
"message",
"login",
"logout",
"encode",
"decode"
"auth",
"nav"
]
ipcRenderer.setMaxListeners(250);

View file

@ -3,9 +3,12 @@ const { Notification, ipcMain } = require("electron");
const store = require("./utils/store");
const { backgroundMitt, ipcEmit } = require("./utils/emitter");
const { connectToPlatform, disconnectFromPlatform } = require("./io");
const { initAudio, terminateAudio, playback, streamState } = require("./audio");
// const { initAudio, terminateAudio, playback, streamState } = require("./audio");
let ui; /* wait to receive ui from the platform */
const util = require('util');
let ui;
let current;
const messageQueue = [];
@ -16,58 +19,67 @@ let processContentId;
* - New message (add message to ui.messages)
* - New content (add content to existing message)
* - Update person
* - Notification
*/
backgroundMitt.on("message", (message) => messageQueue.push(message));
function handleMessageQueue()
{
if (!streamState.pb && !streamState.rec)
{
// if (!streamState.pb && !streamState.rec)
// {
const update = messageQueue.shift();
if (update)
{
console.log("Handling UI Update:", update);
console.log(update);
if (update)
{
if (update.person)
{
ui = update;
ui = update;
current = update.messages.at(-1);
}
else if (update.name)
{
ui.person = update;
}
else if (update.content)
else if (update.modifier)
{
ui.messages.push(update);
current = update;
}
else if (update.category)
{
current.content.push(update);
}
else if (update.context)
{
current.context = update.context;
}
else if (update.text)
{
current.content.at(-1).text = update.text;
}
else
{
ui.messages[ui.messages.length - 1].content.push(update);
const content = current.content.at(-1);
new Notification({
title: current.context,
body: content.text
}).show();
/* Ideally, we want a more native solution than this */
// if (current.modifier == "ai" && content.category == "audio")
// {
// playback(content.blob, current.id);
// }
return;
}
ipcEmit("message", update);
// notify();
}
}
}
function notify()
{
const message = ui.messages[ui.messages.length - 1];
const content = message.content[message.content.length -1];
new Notification(
{
title: message.context,
body: content.text
}
).show();
if (content.category == "audio/L16") playback(content.blob, message.id);
// }
}
function launchSession(account)
@ -93,5 +105,3 @@ ipcMain.on("messenger", () => {
exports.launchSession = launchSession;
exports.endSession = endSession;
// ui.messages[ui.messages.findIndex(m => m.id === message.id)].content.push(message);

View file

@ -5,7 +5,7 @@ const { Tray } = require("electron");
const { backgroundMitt } = require("./utils/emitter");
let tray;
const TRAY_PATH = "./src/assets/tray/";
const TRAY_PATH = path.join(__dirname, "assets/tray");
/* Current state of recording, playback, and ws connection */
const state = {
@ -19,25 +19,24 @@ const state = {
* @param {"recording" | "playback" | "disconnect"} attribute
* @param {boolean} newState
*/
function updateTray(attribute, newState) {
function updateTray(attribute, newState)
{
state[attribute] = newState;
// convert boolean to number
const bi = (b) => b ? 1 : 0;
// Make sure we have the icon we're looking for.
tray.setImage(
TRAY_PATH +
tray.setImage(path.join(
TRAY_PATH,
`${bi(state.recording)}${bi(state.playback)}${bi(state.disconnect)}` +
".png"
);
));
}
function initTray()
function initTray()
{
tray = new Tray(TRAY_PATH + "000" + ".png");
tray = new Tray(path.join(TRAY_PATH, "000.png"));
}
exports.initTray = initTray;

View file

@ -21,28 +21,11 @@ function updateWindowPosition()
}
function createPreviewWin(_e, content)
function onNav(_e, cmd)
{
console.log("Creating preview window");
const preview = new BrowserWindow({
width: 500,
height: 300,
x: null,
y: null,
resizable: true,
frame: false,
webPreferences: { preload: path.join(__dirname, "/render/preload.js") }
});
preview.loadFile(path.join(__dirname, "preview.html"));
preview.webContents.on("did-finish-load", () => {
preview.webContents.send("preview-init", content);
});
cmd === "close" ? win.close() : win.minimize();
}
function createWin()
{
if (win) return;
@ -56,11 +39,11 @@ function createWin()
x: pos.x,
y: pos.y,
resizable: true,
backgroundColor: "#EBEBEB",
frame: false,
minWidth: 350,
minHeight: 500,
webPreferences: { preload: path.join(__dirname, "/render/preload.js") }
backgroundColor: "#EBEBEB",
webPreferences: { preload: path.join(__dirname, "render/preload.js") }
})
// TODO: find a simpler way to notify platform of user events
@ -88,10 +71,10 @@ function createWin()
}
});
win.webContents.send("set-account", store.get("account"));
win.webContents.send("account", store.get("account"));
});
}
ipcMain.on("preview", createPreviewWin);
ipcMain.on("nav", onNav);
exports.createWin = createWin;

785
yarn.lock
View file

@ -1,785 +0,0 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@crimata/nodeaudio@^0.0.0":
"integrity" "sha1-KTpDGuL8j3x72BLNb4Iyg8zgARo="
"resolved" "https://gitlab.com/api/v4/projects/28849281/packages/npm/@crimata/nodeaudio/-/@crimata/nodeaudio-0.0.0.tgz"
"version" "0.0.0"
dependencies:
"bindings" "~1.2.1"
"sleep" "^6.3.0"
"@electron/get@^1.0.1":
"integrity" "sha512-+SjZhRuRo+STTO1Fdhzqnv9D2ZhjxXP6egsJ9kiO8dtP68cDx7dFCwWi64dlMQV7sWcfW1OYCW4wviEBzmRsfQ=="
"resolved" "https://registry.npmjs.org/@electron/get/-/get-1.13.0.tgz"
"version" "1.13.0"
dependencies:
"debug" "^4.1.1"
"env-paths" "^2.2.0"
"fs-extra" "^8.1.0"
"got" "^9.6.0"
"progress" "^2.0.3"
"semver" "^6.2.0"
"sumchecker" "^3.0.1"
optionalDependencies:
"global-agent" "^2.0.2"
"global-tunnel-ng" "^2.7.1"
"@sindresorhus/is@^0.14.0":
"integrity" "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ=="
"resolved" "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz"
"version" "0.14.0"
"@szmarczak/http-timer@^1.1.2":
"integrity" "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA=="
"resolved" "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz"
"version" "1.1.2"
dependencies:
"defer-to-connect" "^1.0.1"
"@types/node@^14.6.2":
"integrity" "sha512-vhUqgjJR1qxwTWV5Ps5txuy2XMdf7Fw+OrdChRboy8BmWUPkckOhphaohzFG6b8DW7CrxaBMdrdJ47SYFq1okw=="
"resolved" "https://registry.npmjs.org/@types/node/-/node-14.17.12.tgz"
"version" "14.17.12"
"ajv-formats@^2.0.2":
"integrity" "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="
"resolved" "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz"
"version" "2.1.1"
dependencies:
"ajv" "^8.0.0"
"ajv@^8.0.0", "ajv@^8.1.0":
"integrity" "sha512-9807RlWAgT564wT+DjeyU5OFMPjmzxVobvDFmNAhY+5zD6A2ly3jDp6sgnfyDtlIQ+7H97oc/DGCzzfu9rjw9w=="
"resolved" "https://registry.npmjs.org/ajv/-/ajv-8.6.2.tgz"
"version" "8.6.2"
dependencies:
"fast-deep-equal" "^3.1.1"
"json-schema-traverse" "^1.0.0"
"require-from-string" "^2.0.2"
"uri-js" "^4.2.2"
"atomically@^1.7.0":
"integrity" "sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w=="
"resolved" "https://registry.npmjs.org/atomically/-/atomically-1.7.0.tgz"
"version" "1.7.0"
"axios@^0.21.1":
"integrity" "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA=="
"resolved" "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz"
"version" "0.21.1"
dependencies:
"follow-redirects" "^1.10.0"
"base64-arraybuffer@^1.0.1":
"integrity" "sha512-vFIUq7FdLtjZMhATwDul5RZWv2jpXQ09Pd6jcVEOvIsqCWTRFD/ONHNfyOS8dA/Ippi5dsIgpyKWKZaAKZltbA=="
"resolved" "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.1.tgz"
"version" "1.0.1"
"bindings@~1.2.1":
"integrity" "sha1-FK1hE4EtLTfXLme0ystLtyZQXxE="
"resolved" "https://registry.npmjs.org/bindings/-/bindings-1.2.1.tgz"
"version" "1.2.1"
"boolean@^3.0.1":
"integrity" "sha512-3hx0kwU3uzG6ReQ3pnaFQPSktpBw6RHN3/ivDKEuU8g1XSfafowyvDnadjv1xp8IZqhtSukxlwv9bF6FhX8m0w=="
"resolved" "https://registry.npmjs.org/boolean/-/boolean-3.1.4.tgz"
"version" "3.1.4"
"buffer-crc32@~0.2.3":
"integrity" "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI="
"resolved" "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz"
"version" "0.2.13"
"buffer-from@^1.0.0":
"integrity" "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="
"resolved" "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz"
"version" "1.1.2"
"cacheable-request@^6.0.0":
"integrity" "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg=="
"resolved" "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz"
"version" "6.1.0"
dependencies:
"clone-response" "^1.0.2"
"get-stream" "^5.1.0"
"http-cache-semantics" "^4.0.0"
"keyv" "^3.0.0"
"lowercase-keys" "^2.0.0"
"normalize-url" "^4.1.0"
"responselike" "^1.0.2"
"clone-response@^1.0.2":
"integrity" "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws="
"resolved" "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz"
"version" "1.0.2"
dependencies:
"mimic-response" "^1.0.0"
"concat-stream@^1.6.2":
"integrity" "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw=="
"resolved" "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz"
"version" "1.6.2"
dependencies:
"buffer-from" "^1.0.0"
"inherits" "^2.0.3"
"readable-stream" "^2.2.2"
"typedarray" "^0.0.6"
"conf@^10.0.0":
"integrity" "sha512-iyy4ArqyQ/yrzNASNBN+jaylu53JRuq0ztvL6KAWYHj4iN56BVuhy2SrzEEHBodNbacZr2Pd/4nWhoAwc66T1g=="
"resolved" "https://registry.npmjs.org/conf/-/conf-10.0.2.tgz"
"version" "10.0.2"
dependencies:
"ajv" "^8.1.0"
"ajv-formats" "^2.0.2"
"atomically" "^1.7.0"
"debounce-fn" "^4.0.0"
"dot-prop" "^6.0.1"
"env-paths" "^2.2.1"
"json-schema-typed" "^7.0.3"
"onetime" "^5.1.2"
"pkg-up" "^3.1.0"
"semver" "^7.3.5"
"config-chain@^1.1.11":
"integrity" "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ=="
"resolved" "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz"
"version" "1.1.13"
dependencies:
"ini" "^1.3.4"
"proto-list" "~1.2.1"
"core-js@^3.6.5":
"integrity" "sha512-lM3GftxzHNtPNUJg0v4pC2RC6puwMd6VZA7vXUczi+SKmCWSf4JwO89VJGMqbzmB7jlK7B5hr3S64PqwFL49cA=="
"resolved" "https://registry.npmjs.org/core-js/-/core-js-3.16.3.tgz"
"version" "3.16.3"
"core-util-is@~1.0.0":
"integrity" "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
"resolved" "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz"
"version" "1.0.2"
"debounce-fn@^4.0.0":
"integrity" "sha512-8pYCQiL9Xdcg0UPSD3d+0KMlOjp+KGU5EPwYddgzQ7DATsg4fuUDjQtsYLmWjnk2obnNHgV3vE2Y4jejSOJVBQ=="
"resolved" "https://registry.npmjs.org/debounce-fn/-/debounce-fn-4.0.0.tgz"
"version" "4.0.0"
dependencies:
"mimic-fn" "^3.0.0"
"debug@^2.6.9":
"integrity" "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="
"resolved" "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz"
"version" "2.6.9"
dependencies:
"ms" "2.0.0"
"debug@^4.1.0", "debug@^4.1.1":
"integrity" "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw=="
"resolved" "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz"
"version" "4.3.2"
dependencies:
"ms" "2.1.2"
"decompress-response@^3.3.0":
"integrity" "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M="
"resolved" "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz"
"version" "3.3.0"
dependencies:
"mimic-response" "^1.0.0"
"defer-to-connect@^1.0.1":
"integrity" "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ=="
"resolved" "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz"
"version" "1.1.3"
"define-properties@^1.1.3":
"integrity" "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ=="
"resolved" "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz"
"version" "1.1.3"
dependencies:
"object-keys" "^1.0.12"
"detect-node@^2.0.4":
"integrity" "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g=="
"resolved" "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz"
"version" "2.1.0"
"dot-prop@^6.0.1":
"integrity" "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA=="
"resolved" "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz"
"version" "6.0.1"
dependencies:
"is-obj" "^2.0.0"
"duplexer3@^0.1.4":
"integrity" "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI="
"resolved" "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz"
"version" "0.1.4"
"electron-store@^8.0.0":
"integrity" "sha512-ZgRPUZkfrrjWSqxZeaxu7lEvmYf6tgl49dLMqxXGnEmliSiwv3u4rJPG+mH3fBQP9PBqgSh4TCuxHZImMMUgWg=="
"resolved" "https://registry.npmjs.org/electron-store/-/electron-store-8.0.0.tgz"
"version" "8.0.0"
dependencies:
"conf" "^10.0.0"
"type-fest" "^1.0.2"
"electron@^13.2.3":
"integrity" "sha512-FzWhbKHjq7ZTpPQFaYiLPL64erC8/BOsu5NlNN9nQ6f7rIP8ygKlNAlQit3vbOcksQAwItDUCIw4sW0mcaRpCA=="
"resolved" "https://registry.npmjs.org/electron/-/electron-13.2.3.tgz"
"version" "13.2.3"
dependencies:
"@electron/get" "^1.0.1"
"@types/node" "^14.6.2"
"extract-zip" "^1.0.3"
"encodeurl@^1.0.2":
"integrity" "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
"resolved" "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz"
"version" "1.0.2"
"end-of-stream@^1.1.0":
"integrity" "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q=="
"resolved" "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz"
"version" "1.4.4"
dependencies:
"once" "^1.4.0"
"env-paths@^2.2.0", "env-paths@^2.2.1":
"integrity" "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="
"resolved" "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz"
"version" "2.2.1"
"es6-error@^4.1.1":
"integrity" "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg=="
"resolved" "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz"
"version" "4.1.1"
"escape-string-regexp@^4.0.0":
"integrity" "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="
"resolved" "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz"
"version" "4.0.0"
"extract-zip@^1.0.3":
"integrity" "sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA=="
"resolved" "https://registry.npmjs.org/extract-zip/-/extract-zip-1.7.0.tgz"
"version" "1.7.0"
dependencies:
"concat-stream" "^1.6.2"
"debug" "^2.6.9"
"mkdirp" "^0.5.4"
"yauzl" "^2.10.0"
"fast-deep-equal@^3.1.1":
"integrity" "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
"resolved" "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz"
"version" "3.1.3"
"fd-slicer@~1.1.0":
"integrity" "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4="
"resolved" "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz"
"version" "1.1.0"
dependencies:
"pend" "~1.2.0"
"find-up@^3.0.0":
"integrity" "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg=="
"resolved" "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz"
"version" "3.0.0"
dependencies:
"locate-path" "^3.0.0"
"follow-redirects@^1.10.0":
"integrity" "sha512-yLR6WaE2lbF0x4K2qE2p9PEXKLDjUjnR/xmjS3wHAYxtlsI9MLLBJUZirAHKzUZDGLxje7w/cXR49WOUo4rbsA=="
"resolved" "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.2.tgz"
"version" "1.14.2"
"fs-extra@^8.1.0":
"integrity" "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="
"resolved" "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz"
"version" "8.1.0"
dependencies:
"graceful-fs" "^4.2.0"
"jsonfile" "^4.0.0"
"universalify" "^0.1.0"
"get-stream@^4.1.0":
"integrity" "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w=="
"resolved" "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz"
"version" "4.1.0"
dependencies:
"pump" "^3.0.0"
"get-stream@^5.1.0":
"integrity" "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="
"resolved" "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz"
"version" "5.2.0"
dependencies:
"pump" "^3.0.0"
"global-agent@^2.0.2":
"integrity" "sha512-+20KpaW6DDLqhG7JDiJpD1JvNvb8ts+TNl7BPOYcURqCrXqnN1Vf+XVOrkKJAFPqfX+oEhsdzOj1hLWkBTdNJg=="
"resolved" "https://registry.npmjs.org/global-agent/-/global-agent-2.2.0.tgz"
"version" "2.2.0"
dependencies:
"boolean" "^3.0.1"
"core-js" "^3.6.5"
"es6-error" "^4.1.1"
"matcher" "^3.0.0"
"roarr" "^2.15.3"
"semver" "^7.3.2"
"serialize-error" "^7.0.1"
"global-tunnel-ng@^2.7.1":
"integrity" "sha512-4s+DyciWBV0eK148wqXxcmVAbFVPqtc3sEtUE/GTQfuU80rySLcMhUmHKSHI7/LDj8q0gDYI1lIhRRB7ieRAqg=="
"resolved" "https://registry.npmjs.org/global-tunnel-ng/-/global-tunnel-ng-2.7.1.tgz"
"version" "2.7.1"
dependencies:
"encodeurl" "^1.0.2"
"lodash" "^4.17.10"
"npm-conf" "^1.1.3"
"tunnel" "^0.0.6"
"globalthis@^1.0.1":
"integrity" "sha512-ZQnSFO1la8P7auIOQECnm0sSuoMeaSq0EEdXMBFF2QJO4uNcwbyhSgG3MruWNbFTqCLmxVwGOl7LZ9kASvHdeQ=="
"resolved" "https://registry.npmjs.org/globalthis/-/globalthis-1.0.2.tgz"
"version" "1.0.2"
dependencies:
"define-properties" "^1.1.3"
"got@^9.6.0":
"integrity" "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q=="
"resolved" "https://registry.npmjs.org/got/-/got-9.6.0.tgz"
"version" "9.6.0"
dependencies:
"@sindresorhus/is" "^0.14.0"
"@szmarczak/http-timer" "^1.1.2"
"cacheable-request" "^6.0.0"
"decompress-response" "^3.3.0"
"duplexer3" "^0.1.4"
"get-stream" "^4.1.0"
"lowercase-keys" "^1.0.1"
"mimic-response" "^1.0.1"
"p-cancelable" "^1.0.0"
"to-readable-stream" "^1.0.0"
"url-parse-lax" "^3.0.0"
"graceful-fs@^4.1.6", "graceful-fs@^4.2.0":
"integrity" "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg=="
"resolved" "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz"
"version" "4.2.8"
"http-cache-semantics@^4.0.0":
"integrity" "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ=="
"resolved" "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz"
"version" "4.1.0"
"inherits@^2.0.3", "inherits@~2.0.3":
"integrity" "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
"resolved" "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz"
"version" "2.0.4"
"ini@^1.3.4":
"integrity" "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="
"resolved" "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz"
"version" "1.3.8"
"is-obj@^2.0.0":
"integrity" "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w=="
"resolved" "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz"
"version" "2.0.0"
"isarray@~1.0.0":
"integrity" "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
"resolved" "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz"
"version" "1.0.0"
"[email protected]":
"integrity" "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg="
"resolved" "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz"
"version" "3.0.0"
"json-schema-traverse@^1.0.0":
"integrity" "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
"resolved" "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz"
"version" "1.0.0"
"json-schema-typed@^7.0.3":
"integrity" "sha512-7DE8mpG+/fVw+dTpjbxnx47TaMnDfOI1jwft9g1VybltZCduyRQPJPvc+zzKY9WPHxhPWczyFuYa6I8Mw4iU5A=="
"resolved" "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-7.0.3.tgz"
"version" "7.0.3"
"json-stringify-safe@^5.0.1":
"integrity" "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus="
"resolved" "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz"
"version" "5.0.1"
"jsonfile@^4.0.0":
"integrity" "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss="
"resolved" "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz"
"version" "4.0.0"
optionalDependencies:
"graceful-fs" "^4.1.6"
"keyv@^3.0.0":
"integrity" "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA=="
"resolved" "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz"
"version" "3.1.0"
dependencies:
"json-buffer" "3.0.0"
"locate-path@^3.0.0":
"integrity" "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A=="
"resolved" "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz"
"version" "3.0.0"
dependencies:
"p-locate" "^3.0.0"
"path-exists" "^3.0.0"
"lodash@^4.17.10":
"integrity" "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
"resolved" "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz"
"version" "4.17.21"
"lowercase-keys@^1.0.0", "lowercase-keys@^1.0.1":
"integrity" "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA=="
"resolved" "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz"
"version" "1.0.1"
"lowercase-keys@^2.0.0":
"integrity" "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA=="
"resolved" "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz"
"version" "2.0.0"
"lru-cache@^6.0.0":
"integrity" "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="
"resolved" "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz"
"version" "6.0.0"
dependencies:
"yallist" "^4.0.0"
"matcher@^3.0.0":
"integrity" "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng=="
"resolved" "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz"
"version" "3.0.0"
dependencies:
"escape-string-regexp" "^4.0.0"
"mimic-fn@^2.1.0":
"integrity" "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="
"resolved" "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz"
"version" "2.1.0"
"mimic-fn@^3.0.0":
"integrity" "sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ=="
"resolved" "https://registry.npmjs.org/mimic-fn/-/mimic-fn-3.1.0.tgz"
"version" "3.1.0"
"mimic-response@^1.0.0", "mimic-response@^1.0.1":
"integrity" "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ=="
"resolved" "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz"
"version" "1.0.1"
"minimist@^1.2.5":
"integrity" "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
"resolved" "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz"
"version" "1.2.5"
"mkdirp@^0.5.4":
"integrity" "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ=="
"resolved" "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz"
"version" "0.5.5"
dependencies:
"minimist" "^1.2.5"
"[email protected]":
"integrity" "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
"resolved" "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz"
"version" "2.0.0"
"[email protected]":
"integrity" "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
"resolved" "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz"
"version" "2.1.2"
"nan@^2.14.1":
"integrity" "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ=="
"resolved" "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz"
"version" "2.15.0"
"normalize-url@^4.1.0":
"integrity" "sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA=="
"resolved" "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.1.tgz"
"version" "4.5.1"
"npm-conf@^1.1.3":
"integrity" "sha512-Yic4bZHJOt9RCFbRP3GgpqhScOY4HH3V2P8yBj6CeYq118Qr+BLXqT2JvpJ00mryLESpgOxf5XlFv4ZjXxLScw=="
"resolved" "https://registry.npmjs.org/npm-conf/-/npm-conf-1.1.3.tgz"
"version" "1.1.3"
dependencies:
"config-chain" "^1.1.11"
"pify" "^3.0.0"
"object-keys@^1.0.12":
"integrity" "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="
"resolved" "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz"
"version" "1.1.1"
"once@^1.3.1", "once@^1.4.0":
"integrity" "sha1-WDsap3WWHUsROsF9nFC6753Xa9E="
"resolved" "https://registry.npmjs.org/once/-/once-1.4.0.tgz"
"version" "1.4.0"
dependencies:
"wrappy" "1"
"onetime@^5.1.2":
"integrity" "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="
"resolved" "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz"
"version" "5.1.2"
dependencies:
"mimic-fn" "^2.1.0"
"p-cancelable@^1.0.0":
"integrity" "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw=="
"resolved" "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz"
"version" "1.1.0"
"p-limit@^2.0.0":
"integrity" "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="
"resolved" "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz"
"version" "2.3.0"
dependencies:
"p-try" "^2.0.0"
"p-locate@^3.0.0":
"integrity" "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ=="
"resolved" "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz"
"version" "3.0.0"
dependencies:
"p-limit" "^2.0.0"
"p-try@^2.0.0":
"integrity" "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="
"resolved" "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz"
"version" "2.2.0"
"path-exists@^3.0.0":
"integrity" "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU="
"resolved" "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz"
"version" "3.0.0"
"pend@~1.2.0":
"integrity" "sha1-elfrVQpng/kRUzH89GY9XI4AelA="
"resolved" "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz"
"version" "1.2.0"
"pify@^3.0.0":
"integrity" "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY="
"resolved" "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz"
"version" "3.0.0"
"pkg-up@^3.1.0":
"integrity" "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA=="
"resolved" "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz"
"version" "3.1.0"
dependencies:
"find-up" "^3.0.0"
"prepend-http@^2.0.0":
"integrity" "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc="
"resolved" "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz"
"version" "2.0.0"
"process-nextick-args@~2.0.0":
"integrity" "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
"resolved" "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz"
"version" "2.0.1"
"progress@^2.0.3":
"integrity" "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="
"resolved" "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz"
"version" "2.0.3"
"proto-list@~1.2.1":
"integrity" "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk="
"resolved" "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz"
"version" "1.2.4"
"pump@^3.0.0":
"integrity" "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww=="
"resolved" "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz"
"version" "3.0.0"
dependencies:
"end-of-stream" "^1.1.0"
"once" "^1.3.1"
"punycode@^2.1.0":
"integrity" "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
"resolved" "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz"
"version" "2.1.1"
"readable-stream@^2.2.2":
"integrity" "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw=="
"resolved" "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz"
"version" "2.3.7"
dependencies:
"core-util-is" "~1.0.0"
"inherits" "~2.0.3"
"isarray" "~1.0.0"
"process-nextick-args" "~2.0.0"
"safe-buffer" "~5.1.1"
"string_decoder" "~1.1.1"
"util-deprecate" "~1.0.1"
"require-from-string@^2.0.2":
"integrity" "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="
"resolved" "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz"
"version" "2.0.2"
"responselike@^1.0.2":
"integrity" "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec="
"resolved" "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz"
"version" "1.0.2"
dependencies:
"lowercase-keys" "^1.0.0"
"roarr@^2.15.3":
"integrity" "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A=="
"resolved" "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz"
"version" "2.15.4"
dependencies:
"boolean" "^3.0.1"
"detect-node" "^2.0.4"
"globalthis" "^1.0.1"
"json-stringify-safe" "^5.0.1"
"semver-compare" "^1.0.0"
"sprintf-js" "^1.1.2"
"safe-buffer@~5.1.0", "safe-buffer@~5.1.1":
"integrity" "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
"resolved" "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz"
"version" "5.1.2"
"semver-compare@^1.0.0":
"integrity" "sha1-De4hahyUGrN+nvsXiPavxf9VN/w="
"resolved" "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz"
"version" "1.0.0"
"semver@^6.2.0":
"integrity" "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
"resolved" "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz"
"version" "6.3.0"
"semver@^7.3.2", "semver@^7.3.5":
"integrity" "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ=="
"resolved" "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz"
"version" "7.3.5"
dependencies:
"lru-cache" "^6.0.0"
"serialize-error@^7.0.1":
"integrity" "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw=="
"resolved" "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz"
"version" "7.0.1"
dependencies:
"type-fest" "^0.13.1"
"sleep@^6.3.0":
"integrity" "sha512-+WgYl951qdUlb1iS97UvQ01pkauoBK9ML9I/CMPg41v0Ze4EyMlTgFTDDo32iYj98IYqxIjDMRd+L71lawFfpQ=="
"resolved" "https://registry.npmjs.org/sleep/-/sleep-6.3.0.tgz"
"version" "6.3.0"
dependencies:
"nan" "^2.14.1"
"sprintf-js@^1.1.2":
"integrity" "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug=="
"resolved" "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz"
"version" "1.1.2"
"string_decoder@~1.1.1":
"integrity" "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="
"resolved" "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz"
"version" "1.1.1"
dependencies:
"safe-buffer" "~5.1.0"
"sumchecker@^3.0.1":
"integrity" "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg=="
"resolved" "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz"
"version" "3.0.1"
dependencies:
"debug" "^4.1.0"
"to-readable-stream@^1.0.0":
"integrity" "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q=="
"resolved" "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz"
"version" "1.0.0"
"tunnel@^0.0.6":
"integrity" "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="
"resolved" "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz"
"version" "0.0.6"
"type-fest@^0.13.1":
"integrity" "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="
"resolved" "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz"
"version" "0.13.1"
"type-fest@^1.0.2":
"integrity" "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA=="
"resolved" "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz"
"version" "1.4.0"
"typedarray@^0.0.6":
"integrity" "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
"resolved" "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz"
"version" "0.0.6"
"universalify@^0.1.0":
"integrity" "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="
"resolved" "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz"
"version" "0.1.2"
"uri-js@^4.2.2":
"integrity" "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="
"resolved" "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz"
"version" "4.4.1"
dependencies:
"punycode" "^2.1.0"
"url-parse-lax@^3.0.0":
"integrity" "sha1-FrXK/Afb42dsGxmZF3gj1lA6yww="
"resolved" "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz"
"version" "3.0.0"
dependencies:
"prepend-http" "^2.0.0"
"util-deprecate@~1.0.1":
"integrity" "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
"resolved" "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz"
"version" "1.0.2"
"wrappy@1":
"integrity" "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
"resolved" "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz"
"version" "1.0.2"
"ws@^7.3.1":
"integrity" "sha512-zP9z6GXm6zC27YtspwH99T3qTG7bBFv2VIkeHstMLrLlDJuzA7tQ5ls3OJ1hOGGCzTQPniNJoHXIAOS0Jljohg=="
"resolved" "https://registry.npmjs.org/ws/-/ws-7.5.4.tgz"
"version" "7.5.4"
"yallist@^4.0.0":
"integrity" "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
"resolved" "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz"
"version" "4.0.0"
"yauzl@^2.10.0":
"integrity" "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk="
"resolved" "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz"
"version" "2.10.0"
dependencies:
"buffer-crc32" "~0.2.3"
"fd-slicer" "~1.1.0"