WIP:playback queue and tray icon states
|
Before Width: | Height: | Size: 9.1 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 4 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 4 KiB |
|
Before Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 105 KiB |
BIN
build/tray/000_16x16@2x.png
Normal file
|
After Width: | Height: | Size: 4 KiB |
BIN
build/tray/001_16x16@2x.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
build/tray/010_16x16@2x.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
build/tray/011_16x16@2x.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
build/tray/100_16x16@2x.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
build/tray/101_16x16@2x.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
build/tray/110_16x16@2x.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
build/tray/111_16x16@2x.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
|
|
@ -28,7 +28,6 @@
|
|||
"electron-is-dev": "^2.0.0",
|
||||
"electron-store": "^8.0.0",
|
||||
"electron-updater": "^4.3.8",
|
||||
"iohook": "^0.9.3",
|
||||
"mitt": "^2.1.0",
|
||||
"update-electron-app": "^2.0.1",
|
||||
"vue": "^3.0.0-0",
|
||||
|
|
|
|||
|
|
@ -114,8 +114,6 @@ export const updateAppState = () => {
|
|||
|
||||
ipcEmit("set-version", app.getVersion());
|
||||
|
||||
updateAppUI();
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
139
src/audio.ts
|
|
@ -1,5 +1,8 @@
|
|||
const nodeAudio = require('@crimata/nodeaudio');
|
||||
import { globalShortcut } from "electron";
|
||||
const nodeAudio = require('@crimata/nodeaudio');
|
||||
|
||||
import { updateTray } from "@/tray";
|
||||
import { sendMessage } from "@/session";
|
||||
import { backgroundMitt, ipcEmit } from '@/composables/useEmitter';
|
||||
|
||||
let inputDevice: number;
|
||||
|
|
@ -7,68 +10,100 @@ let outputDevice: number;
|
|||
|
||||
let setStreamsId: ReturnType<typeof setTimeout>;
|
||||
|
||||
let playbackEl;
|
||||
let writeToOBuffer = false;
|
||||
let recording = false;
|
||||
const chunks: Int16Array[] = [];
|
||||
|
||||
// backgroundMitt.on("data", (int16Arr: Int16Array) => {
|
||||
// if (recording) {
|
||||
// chunks.push(int16Arr);
|
||||
// ipcEmit("recording", int16Arr); // for recording animation
|
||||
// }
|
||||
// });
|
||||
backgroundMitt.on("data", (int16Arr: Int16Array) => {
|
||||
if (recording) {
|
||||
chunks.push(int16Arr);
|
||||
ipcEmit("recording", int16Arr);
|
||||
}
|
||||
});
|
||||
|
||||
// const setStreams = () => {
|
||||
backgroundMitt.on("playback", (uid: string, aplitude: number) => {
|
||||
ipcEmit("animate-playback", { uid: uid, aplitude: aplitude });
|
||||
});
|
||||
|
||||
// const defaultInput = nodeAudio.GetDefaultInputDevice();
|
||||
// const defaultOutput = nodeAudio.GetDefaultOutputDevice();
|
||||
backgroundMitt.on("write", (status: boolean) => {
|
||||
writeToOBuffer = status;
|
||||
});
|
||||
|
||||
// if (inputDevice !== defaultInput) {
|
||||
// inputDevice = defaultInput;
|
||||
// nodeAudio.CloseInputStream(inputDevice);
|
||||
// nodeAudio.OpenInputStream(inputDevice);
|
||||
// }
|
||||
const setStreams = () => {
|
||||
|
||||
// if (outputDevice !== defaultOutput) {
|
||||
// outputDevice = defaultOutput;
|
||||
// nodeAudio.CloseOutputStream(outputDevice);
|
||||
// nodeAudio.OpenOutputStream(outputDevice);
|
||||
// }
|
||||
// }
|
||||
const defaultInput = nodeAudio.core.GetDefaultInputDevice();
|
||||
const defaultOutput = nodeAudio.core.GetDefaultOutputDevice();
|
||||
|
||||
// const startRecording = () => {
|
||||
// recording = true;
|
||||
// }
|
||||
|
||||
// const stopRecording = () => {
|
||||
// recording = false;
|
||||
// const audio: Int16Array = nodeAudio.MergeChunks(chunks);
|
||||
// backgroundMitt.emit("audio", audio); // emit audio event
|
||||
// chunks.length = 0;
|
||||
// }
|
||||
|
||||
export const initAudio = () => {
|
||||
// nodeAudio.Initialize(backgroundMitt.emit.bind(backgroundMitt));
|
||||
// setStreamsId = setInterval(setStreams, 2000); // sense device
|
||||
|
||||
const ret = globalShortcut.register('CommandOrControl+R', () => {
|
||||
console.log('Toggle record...');
|
||||
});
|
||||
|
||||
if (!ret) {
|
||||
console.log('registration failed');
|
||||
if (inputDevice !== defaultInput) {
|
||||
inputDevice = defaultInput;
|
||||
nodeAudio.core.CloseInputStream(inputDevice);
|
||||
nodeAudio.core.OpenInputStream(inputDevice);
|
||||
}
|
||||
|
||||
// Check whether a shortcut is registered.
|
||||
console.log(globalShortcut.isRegistered('CommandOrControl+R'));
|
||||
if (outputDevice !== defaultOutput) {
|
||||
outputDevice = defaultOutput;
|
||||
nodeAudio.core.CloseOutputStream(outputDevice);
|
||||
nodeAudio.core.OpenOutputStream(outputDevice);
|
||||
}
|
||||
}
|
||||
|
||||
const startRecording = () => {
|
||||
|
||||
/* If playback is happening, we kill it */
|
||||
if (playback) nodeAudio.core.ClearOutputBuffer();
|
||||
|
||||
recording = true;
|
||||
updateTray("recording", recording);
|
||||
}
|
||||
|
||||
const stopRecording = () => {
|
||||
recording = false;
|
||||
updateTray("recording", recording);
|
||||
|
||||
/* Send the data to the platform right away */
|
||||
sendMessage({
|
||||
text: false,
|
||||
audio: nodeAudio.utils.mergeChunks(chunks)
|
||||
} as Raw);
|
||||
|
||||
chunks.length = 0;
|
||||
}
|
||||
|
||||
export const initAudio = () => {
|
||||
nodeAudio.core.Initialize(backgroundMitt.emit.bind(backgroundMitt));
|
||||
setStreamsId = setInterval(setStreams, 2000); // sense device
|
||||
|
||||
/* Toggle recording switch with global shortcut */
|
||||
globalShortcut.register('CommandOrControl+R', () => {
|
||||
if (recording) {
|
||||
stopRecording();
|
||||
} else {
|
||||
startRecording();
|
||||
};
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
// export const playback = (audio: Int16Array) => {
|
||||
// console.log("Playing audio");
|
||||
// // nodeAudio.WriteToOutputStream(audio);
|
||||
// }
|
||||
export const playback = (audio: Int16Array, source: string) => {
|
||||
|
||||
// export const terminateAudio = () => {
|
||||
// console.log("Terminating audio");
|
||||
// // clearInterval(setStreamsId);
|
||||
// }
|
||||
/* If existing playback is happening, we kill it */
|
||||
if (playback) nodeAudio.core.ClearOutputBuffer();
|
||||
|
||||
const playbackEl = uid;
|
||||
|
||||
nodeAudio.core.WriteToOutputStream(audio.buffer);
|
||||
}
|
||||
|
||||
export const terminateAudio = () => {
|
||||
clearInterval(setStreamsId);
|
||||
nodeAudio.core.Terminate();
|
||||
}
|
||||
|
||||
export const getStreamState = () => {
|
||||
let busy = false;
|
||||
if (playback || recording) {
|
||||
busy = true;
|
||||
}
|
||||
return busy;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import createWindow from "./window";
|
|||
import main from "./main";
|
||||
import { backgroundMitt } from '@/composables/useEmitter';
|
||||
import { setWindowOpen, getWindowOpen } from './store';
|
||||
import { terminateAudio } from './audio';
|
||||
|
||||
console.log('Starting Crimata electron app.');
|
||||
|
||||
|
|
@ -31,6 +32,7 @@ app.on("ready", async () => {
|
|||
});
|
||||
|
||||
app.on("will-quit", () => {
|
||||
terminateAudio();
|
||||
globalShortcut.unregisterAll();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
import { IpcListener } from "@/composables/useIpcMain"
|
||||
import { sendMessage } from '@/session';
|
||||
import { updateAppState } from "@/account";
|
||||
import { playback } from "@/audio";
|
||||
import { onNavBar } from "@/window";
|
||||
import { sendMessage } from '@/session';
|
||||
import { setWindowFocus } from '@/store';
|
||||
import { updateAppState } from "@/account";
|
||||
import { IpcListener } from "@/composables/useIpcMain"
|
||||
|
||||
const CLIENT_MESSAGE_CHANNEL = "post-session-send"
|
||||
const APP_MOUNT_CHANNEL = "post-app-mount";
|
||||
const NAV_BAR_CHANNEL = "post-nav-bar";
|
||||
const POST_WINDOW_FOCUS = 'post-window-focus';
|
||||
const PLAYBACK_CHANNEL = "post-playback;"
|
||||
|
||||
export const messageListener = new IpcListener<Raw | Request>({
|
||||
channel: CLIENT_MESSAGE_CHANNEL,
|
||||
|
|
@ -28,3 +30,8 @@ export const windowFocusListener = new IpcListener<FocusPayload>({
|
|||
channel: POST_WINDOW_FOCUS,
|
||||
listenerCallback: ({ isFocused }) => setWindowFocus(isFocused)
|
||||
});
|
||||
|
||||
export const playbackListener = new IpcListener({
|
||||
channel: PLAYBACK_CHANNEL,
|
||||
listenerCallback: playback
|
||||
});
|
||||
|
|
|
|||
17
src/main.ts
|
|
@ -1,18 +1,7 @@
|
|||
/**
|
||||
* Where the background logic really begins, gets called by app.onReady().
|
||||
*
|
||||
* Handles authentication. If profile is set, we launch a session, which consis
|
||||
* of opening a connection with the platform, initializing the audio streams.
|
||||
*
|
||||
* The session is primarily an interface between the frontend and the platform,
|
||||
* relaying messages from one to the other.
|
||||
*
|
||||
*/
|
||||
|
||||
import { initTray } from '@/tray';
|
||||
import createWindow from "@/window";
|
||||
import initIpcMain from "@/ipc/index";
|
||||
import { accountAuth } from "./account";
|
||||
import createWindow from "./window";
|
||||
import { initTray } from './tray';
|
||||
import { accountAuth } from "@/account";
|
||||
|
||||
export default async function main() {
|
||||
|
||||
|
|
|
|||
81
src/render/audioVisualizer.js
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
class AudioVisualizer {
|
||||
constructor( audioContext, processFrame, processError ) {
|
||||
this.audioContext = audioContext;
|
||||
this.processFrame = processFrame;
|
||||
this.connectStream = this.connectStream.bind( this );
|
||||
navigator.mediaDevices.getUserMedia( { audio: true, video: false } )
|
||||
.then( this.connectStream )
|
||||
.catch( ( error ) => {
|
||||
if ( processError ) {
|
||||
processError( error );
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
connectStream( stream ) {
|
||||
this.analyser = this.audioContext.createAnalyser();
|
||||
const source = this.audioContext.createMediaStreamSource( stream );
|
||||
source.connect( this.analyser );
|
||||
this.analyser.smoothingTimeConstant = 0.5;
|
||||
this.analyser.fftSize = 32;
|
||||
|
||||
this.initRenderLoop( this.analyser );
|
||||
}
|
||||
|
||||
initRenderLoop() {
|
||||
const frequencyData = new Uint8Array( this.analyser.frequencyBinCount );
|
||||
const processFrame = this.processFrame || ( () => {} );
|
||||
|
||||
const renderFrame = () => {
|
||||
this.analyser.getByteFrequencyData( frequencyData );
|
||||
processFrame( frequencyData );
|
||||
|
||||
requestAnimationFrame( renderFrame );
|
||||
};
|
||||
requestAnimationFrame( renderFrame );
|
||||
}
|
||||
}
|
||||
|
||||
const visualMainElement = document.querySelector( 'main' );
|
||||
const visualValueCount = 16;
|
||||
let visualElements;
|
||||
const createDOMElements = () => {
|
||||
let i;
|
||||
for ( i = 0; i < visualValueCount; ++i ) {
|
||||
const elm = document.createElement( 'div' );
|
||||
visualMainElement.appendChild( elm );
|
||||
}
|
||||
|
||||
visualElements = document.querySelectorAll( 'main div' );
|
||||
};
|
||||
createDOMElements();
|
||||
|
||||
const init = () => {
|
||||
// Creating initial DOM elements
|
||||
const audioContext = new AudioContext();
|
||||
const initDOM = () => {
|
||||
visualMainElement.innerHTML = '';
|
||||
createDOMElements();
|
||||
};
|
||||
initDOM();
|
||||
|
||||
// Swapping values around for a better visual effect
|
||||
const dataMap = { 0: 15, 1: 10, 2: 8, 3: 9, 4: 6, 5: 5, 6: 2, 7: 1, 8: 0, 9: 4, 10: 3, 11: 7, 12: 11, 13: 12, 14: 13, 15: 14 };
|
||||
const processFrame = ( data ) => {
|
||||
const values = Object.values( data );
|
||||
let i;
|
||||
for ( i = 0; i < visualValueCount; ++i ) {
|
||||
const value = values[ dataMap[ i ] ] / 255;
|
||||
const elmStyles = visualElements[ i ].style;
|
||||
elmStyles.transform = `scaleY( ${ value } )`;
|
||||
elmStyles.opacity = Math.max( .25, value );
|
||||
}
|
||||
};
|
||||
|
||||
const processError = () => {
|
||||
visualMainElement.classList.add( 'error' );
|
||||
visualMainElement.innerText = 'Please allow access to your microphone in order to see this demo.\nNothing bad is going to happen... hopefully :P';
|
||||
}
|
||||
|
||||
const a = new AudioVisualizer( audioContext, processFrame, processError );
|
||||
};
|
||||
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
<span :class="`context ${modifier}-context`">
|
||||
|
||||
<div v-if="context.img" class="avatar">
|
||||
<img v-if="context.img === 'crimata'" :src="require('@/render/assets/crimataAvatar.svg')">
|
||||
<div v-if="context.img" class="avatar" @click="clickMethod">
|
||||
<img v-click="" v-if="context.img === 'crimata'" :src="require('@/render/assets/crimataAvatar.svg')">
|
||||
<div v-else>{{ context.img }}</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -85,6 +85,14 @@ export function animateAudioInput () {
|
|||
|
||||
}
|
||||
|
||||
/* TODO: animation function that takes in a message ID, and data about the
|
||||
* playedback audio. We target the element with the UID and animate it according
|
||||
* to the data. - still very hypothetical...
|
||||
*/
|
||||
export function animatePlayback (uid: string, meta: number) {
|
||||
console.log("animating playback!")
|
||||
}
|
||||
|
||||
// Given index and length of content, return message child status.
|
||||
export function calcChild (index: number, len: number) {
|
||||
if (len === 1) {
|
||||
|
|
|
|||
|
|
@ -1,77 +0,0 @@
|
|||
import { onMounted, onUnmounted, ref, Ref } from "vue";
|
||||
import { postMessage } from "@/render/ipc";
|
||||
import { animateAudioInput } from "./helpers";
|
||||
import { invokeReturnAudio, postAudioChunk } from "@/render/ipc";
|
||||
|
||||
export default function useAudioInputController (typing: Ref) {
|
||||
|
||||
const recording = ref(false);
|
||||
let mediaRecorder: MediaRecorder;
|
||||
|
||||
const { show, hide } = animateAudioInput();
|
||||
|
||||
// initialize audio
|
||||
const conf = {audio: true, video: false}
|
||||
navigator.mediaDevices.getUserMedia(conf).then((stream: MediaStream) => {
|
||||
|
||||
const options = {mimeType: 'audio/webm'};
|
||||
mediaRecorder = new MediaRecorder(stream, options);
|
||||
|
||||
// post any mew audio to backend
|
||||
mediaRecorder.addEventListener('dataavailable', (e: BlobEvent) => {
|
||||
e.data.arrayBuffer().then((buff: ArrayBuffer) => {
|
||||
postAudioChunk(buff);
|
||||
});
|
||||
});
|
||||
|
||||
// get audio and post new message to backend
|
||||
mediaRecorder.addEventListener('stop', (_e: Event) => {
|
||||
invokeReturnAudio().then((audio: ArrayBuffer[] | Error) => {
|
||||
console.log(audio);
|
||||
// postMessage(newMessage({audio: audio}));
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
// start recording on space bar
|
||||
const record = () => {
|
||||
console.log("INPT:Capturing audio...")
|
||||
mediaRecorder.start();
|
||||
recording.value = true;
|
||||
show()
|
||||
}
|
||||
|
||||
// stop recording and send on release
|
||||
const stop = () => {
|
||||
console.log("INPT:Stopping record.")
|
||||
mediaRecorder.stop();
|
||||
recording.value = false;
|
||||
hide();
|
||||
}
|
||||
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.keyCode == 32 && !typing.value) record();
|
||||
}
|
||||
|
||||
const onKeyUp = (e: KeyboardEvent) => {
|
||||
if (e.keyCode == 32 && recording.value) stop();
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener("keydown", onKeyDown);
|
||||
window.addEventListener("keyup", onKeyUp);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener("keydown", onKeyDown);
|
||||
window.removeEventListener("keyup", onKeyUp);
|
||||
})
|
||||
|
||||
return {
|
||||
recording
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -19,21 +19,19 @@
|
|||
type="text"
|
||||
/>
|
||||
|
||||
<!-- Show suggestions menu on click -->
|
||||
<!-- <Menu /> -->
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
import { defineComponent } from "vue";
|
||||
import { defineComponent, watch } from "vue";
|
||||
import draggify from "@/render/composables/useDraggify";
|
||||
import { profile } from "@/render/shared/profile";
|
||||
import { recording } from "@/render/shared/audio";
|
||||
import { animateAudioInput } from "@/render/components/controllers/helpers";
|
||||
|
||||
import useTextInputController from
|
||||
"@/render/components/controllers/inputItem.control.text";
|
||||
// import useAudioInputController from
|
||||
"@/render/components/controllers/inputItem.control.audio";
|
||||
|
||||
export default defineComponent({
|
||||
|
|
@ -50,8 +48,12 @@ export default defineComponent({
|
|||
|
||||
// Controllers for text and audio.
|
||||
const { typing } = useTextInputController(elementX)
|
||||
// const { recording } = useAudioInputController(typing)
|
||||
const recording = false;
|
||||
|
||||
// Control rec icon (red dot upper right of UI)
|
||||
const { show, hide } = animateAudioInput();
|
||||
watch(recording, (val, _oldval) => {
|
||||
recording ? show() : hide();
|
||||
});
|
||||
|
||||
return {
|
||||
profile,
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ const { post, invoke } = useIpc();
|
|||
*
|
||||
*/
|
||||
|
||||
|
||||
export const invokeLogin = async (
|
||||
payload: AccountCredentials
|
||||
): Promise<Profile | Error> => (
|
||||
|
|
@ -27,12 +26,8 @@ export const invokeLogout = async (): Promise<void> => (
|
|||
*
|
||||
*/
|
||||
|
||||
export const postAudioChunk = (chunk: ArrayBuffer): void => (
|
||||
post("post-audio-collect", chunk)
|
||||
);
|
||||
|
||||
export const invokeReturnAudio = async (): Promise<ArrayBuffer[] | Error> => (
|
||||
await invoke("invoke-audio-flush", null)
|
||||
export const postPlayback = (payload: string): void => (
|
||||
post('post-playback', payload)
|
||||
);
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { setAccount } from "./shared/account";
|
|||
import { setProfile } from "./shared/profile";
|
||||
import { setVersion } from "./shared/version";
|
||||
import { setConnectionStatus } from "./shared/connectionStatus";
|
||||
import { setRecording } from "./shared/audio";
|
||||
|
||||
const SET_ACCOUNT_CHANNEL = "set-account";
|
||||
const SET_PROFILE_CHANNEL = "set-profile";
|
||||
|
|
@ -23,6 +24,7 @@ const ADD_MESSAGE_CHANNEL = "add-message";
|
|||
const UPDATE_MESSAGE_CHANNEL = "update-message";
|
||||
const DELETE_MESSAGE_CHANNEL = "delete-message";
|
||||
const CONNECTION_STATUS_CHANNEL = "set-connection-status";
|
||||
const RECORDING_STATUS_CHANNEL = "recording-status";
|
||||
|
||||
export const setAccountListener = new IpcRendererListener({
|
||||
channel: SET_ACCOUNT_CHANNEL,
|
||||
|
|
@ -63,4 +65,14 @@ export const deleteMessagesListener = new IpcRendererListener({
|
|||
export const connectionStatusListener = new IpcRendererListener({
|
||||
channel: CONNECTION_STATUS_CHANNEL,
|
||||
listenerCallback: setConnectionStatus
|
||||
});
|
||||
|
||||
export const recordingStatusListener = new IpcRendererListener({
|
||||
channel: RECORDING_STATUS_CHANNEL,
|
||||
listenerCallback: setRecording
|
||||
});
|
||||
|
||||
export const animatePlaybackListener = new IpcRendererListener({
|
||||
channel: PLAYBACK_LISTENER,
|
||||
listenerCallback: animatePlayback
|
||||
});
|
||||
12
src/render/shared/audio.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
// shared
|
||||
import { ref } from "vue";
|
||||
|
||||
export const recording = ref();
|
||||
|
||||
export const setRecording: IpcListenerCallback<boolean> = (payload) => {
|
||||
recording.value = payload;
|
||||
}
|
||||
|
||||
export default {
|
||||
recording
|
||||
}
|
||||
145
src/session.ts
|
|
@ -1,117 +1,112 @@
|
|||
import { Notification } from 'electron';
|
||||
|
||||
// import useAudio from "@/audio";
|
||||
import UIState from "@/uiState";
|
||||
import { config } from "@/config";
|
||||
import useWebsockets from "./composables/useWebsockets";
|
||||
import { updateTray } from "@/tray";
|
||||
import { getWindowFocus, getWindowOpen } from '@/store';
|
||||
import useWebsockets from "@/composables/useWebsockets";
|
||||
import { backgroundMitt, ipcEmit } from "@/composables/useEmitter";
|
||||
import { Notification } from 'electron';
|
||||
import { getWindowFocus, getWindowOpen } from './store';
|
||||
import { initAudio, terminateAudio, playback } from "./audio";
|
||||
import { initAudio, terminateAudio, playback, getStreamState } from "@/audio";
|
||||
|
||||
export const CONNECTION_STATUS = {
|
||||
connected: 'Connected',
|
||||
nominal: 'Nominal',
|
||||
reconnecting: 'Reconnecting',
|
||||
lost: 'Connection Lost',
|
||||
};
|
||||
const contentQueue: Update[] = [];
|
||||
|
||||
const uiState = new UIState();
|
||||
|
||||
const onMessageCallback = (payload: string) => {
|
||||
const onMessageCallback = async (payload: string) =>
|
||||
{
|
||||
const message = JSON.parse(payload) as ClientProtocol;
|
||||
|
||||
if (payload === "CLOSE_AUTH_FAIL") {
|
||||
backgroundMitt.emit("close-auth-fail");
|
||||
if (message.header == "init") {
|
||||
uiState.set(message.body as Init);
|
||||
return;
|
||||
}
|
||||
|
||||
const message = JSON.parse(payload) as ClientProtocol;
|
||||
|
||||
if (message.header === "init")
|
||||
if (content)
|
||||
{
|
||||
uiState.set(message.body as Init);
|
||||
contentQueue.push(message.body)
|
||||
}
|
||||
else
|
||||
{
|
||||
const body = message.body as Update;
|
||||
uiState.update(body);
|
||||
|
||||
if (body.name === "add")
|
||||
{
|
||||
const data = body.data as Message;
|
||||
|
||||
// Show a notification if new content and window is not focused.
|
||||
if (!getWindowFocus())
|
||||
{
|
||||
new Notification({
|
||||
title: 'New Message',
|
||||
subtitle: data.context.text as string,
|
||||
body: data.content[0].text,
|
||||
})
|
||||
.show();
|
||||
}
|
||||
|
||||
// Playback audio if message has audio.
|
||||
// TODO: Should we worry about playback and record at the same time?
|
||||
// if (data.content.audio)
|
||||
// {
|
||||
// playback(data.content.audio);
|
||||
// }
|
||||
}
|
||||
uiState.update(message.body as Update);
|
||||
}
|
||||
}
|
||||
|
||||
const statusOptions = { openMessage: 'Connected',
|
||||
pongMessage: 'Nominal',
|
||||
closeMessage: 'Reconnecting',
|
||||
pingErrorMessage: 'Connection Lost' };
|
||||
|
||||
const connectionStatusCallback = (status: string) =>
|
||||
{
|
||||
/* Simplify connectionStatus for tray state */
|
||||
let connectionStatusSimple = false;
|
||||
if (status == statusOptions.pingErrorMessage || statusOptions.closeMessage)
|
||||
{
|
||||
connectionStatusSimple = true;
|
||||
}
|
||||
|
||||
updateTray("disconnect", connectionStatusSimple);
|
||||
|
||||
const connectionStatusCallback = (status: string) => {
|
||||
// backgroundMitt.emit("update-icon-status", status);
|
||||
ipcEmit('set-connection-status', status);
|
||||
}
|
||||
|
||||
const statusOptions = {
|
||||
openMessage: CONNECTION_STATUS.connected,
|
||||
pongMessage: CONNECTION_STATUS.nominal,
|
||||
closeMessage: CONNECTION_STATUS.reconnecting,
|
||||
pingErrorMessage: CONNECTION_STATUS.lost,
|
||||
}
|
||||
const { connect, send, close } = useWebsockets(onMessageCallback,
|
||||
connectionStatusCallback, statusOptions);
|
||||
|
||||
const { connect, send, close } = useWebsockets(
|
||||
onMessageCallback,
|
||||
connectionStatusCallback,
|
||||
statusOptions
|
||||
);
|
||||
|
||||
export const updateAppUI = (): void => {
|
||||
uiState.emit();
|
||||
}
|
||||
|
||||
// Listen for audio recordings and send them to backend.
|
||||
backgroundMitt.on("audio", (audio: Int16Array) => {
|
||||
sendMessage({
|
||||
text: false,
|
||||
audio: audio
|
||||
} as Raw);
|
||||
});
|
||||
|
||||
export function sendMessage(message: Raw | Request): void {
|
||||
export function sendMessage(message: Raw | Request): void
|
||||
{
|
||||
send(message);
|
||||
}
|
||||
|
||||
/* launch a new session (the main process for authenticated users) */
|
||||
export function launchSession(platformKey: string) {
|
||||
let processContentId: ReturnType<typeof setTimeout>;
|
||||
|
||||
/* launch a new session (the main process for authenticated users) */
|
||||
export function launchSession(platformKey: string)
|
||||
{
|
||||
/* connect to the platform */
|
||||
connect(config.PLATFORM_URL, platformKey);
|
||||
|
||||
initAudio();
|
||||
|
||||
processContentId = setInterval(() =>
|
||||
{
|
||||
if (getStreamState())
|
||||
{
|
||||
if (contentQueue.length > 0)
|
||||
{
|
||||
const update = contentQueue.shift() as Update;
|
||||
|
||||
/* Update the UI */
|
||||
uiState.update(update);
|
||||
|
||||
/* Spawn notification if window not in focus */
|
||||
if (!getWindowFocus())
|
||||
{
|
||||
new Notification({
|
||||
title: "New Message",
|
||||
subtitle: data.context.text,
|
||||
body: data.content[0].text
|
||||
}).show();
|
||||
}
|
||||
|
||||
/* Play audio if audio */
|
||||
if (update.data.content[-1].audio)
|
||||
{
|
||||
playback(message.audio, message.uid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
export function endSession() {
|
||||
|
||||
export function endSession()
|
||||
{
|
||||
terminateAudio();
|
||||
|
||||
close(1000, 'session-logout');
|
||||
|
||||
uiState.reset();
|
||||
clearInterval(processContentId)
|
||||
|
||||
uiState.reset();
|
||||
}
|
||||
|
||||
|
|
|
|||
94
src/tray.ts
|
|
@ -1,90 +1,34 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { Tray } from 'electron';
|
||||
const nativeImage = require('electron').nativeImage;
|
||||
import { NativeImage } from 'electron'
|
||||
import { backgroundMitt } from "@/composables/useEmitter";
|
||||
import { getIconState, setIconState } from "@/store";
|
||||
import { CONNECTION_STATUS } from '@/session';
|
||||
|
||||
declare const __static: string;
|
||||
let tray: Tray;
|
||||
const ICON_PATH = "./build/tray";
|
||||
|
||||
interface Icons {
|
||||
[name: string]: NativeImage;
|
||||
/* Current state of recording, playback, and ws connection */
|
||||
const state = {
|
||||
recording: false,
|
||||
playback: false,
|
||||
disconnect: false // whether we're experiencing connection issues
|
||||
}
|
||||
|
||||
let tray: Tray | null = null;
|
||||
const TRAY_ICON_DIR = 'trayIcon';
|
||||
export function updateTray(attribute: "recording" | "playback" | "disconnect", newState: boolean) {
|
||||
|
||||
const ICON_NAMES = {
|
||||
default: 'icon.png',
|
||||
// TODO: need icons for different states
|
||||
// TODO: need higher density icons @3x, @4x, @5x
|
||||
// TODO: need to test with 20x20 & 24x24 icon sizes
|
||||
recording: 'recording.png',
|
||||
playback: 'playback.png',
|
||||
connected: 'connected.png',
|
||||
nominal: 'nominal.png',
|
||||
reconnecting: 'reconnecting.png',
|
||||
lost: 'lost.png',
|
||||
}
|
||||
state[attribute] = newState;
|
||||
|
||||
const TRAY_STATUS = {
|
||||
playback: 'playback',
|
||||
recording: 'recording',
|
||||
...CONNECTION_STATUS
|
||||
};
|
||||
// convert boolean to number
|
||||
const bi = (b: boolean) => b ? 1 : 0;
|
||||
|
||||
// Make sure we have the icon we're looking for.
|
||||
tray.setImage(
|
||||
ICON_PATH +
|
||||
`${bi(state.recording)}${bi(state.playback)}${bi(state.disconnect)}` +
|
||||
".png"
|
||||
);
|
||||
|
||||
const ICONS: Icons = {
|
||||
default: iconFactory(ICON_NAMES.default),
|
||||
[TRAY_STATUS.recording]: iconFactory(ICON_NAMES.recording),
|
||||
[TRAY_STATUS.playback]: iconFactory(ICON_NAMES.playback),
|
||||
[TRAY_STATUS.connected]: iconFactory(ICON_NAMES.connected),
|
||||
[TRAY_STATUS.nominal]: iconFactory(ICON_NAMES.nominal),
|
||||
[TRAY_STATUS.reconnecting]: iconFactory(ICON_NAMES.reconnecting),
|
||||
[TRAY_STATUS.lost]: iconFactory(ICON_NAMES.lost),
|
||||
};
|
||||
|
||||
backgroundMitt.on('update-icon-status', (payload: string) => {
|
||||
updateTray(payload);
|
||||
});
|
||||
|
||||
const changeIcon = (icon: NativeImage): void => tray ? tray.setImage(icon): undefined;
|
||||
|
||||
function iconFactory(iconName: string): NativeImage {
|
||||
if (iconName !== 'icon.png') {
|
||||
console.log('ERROR: Missing Icons: ', iconName);
|
||||
}
|
||||
// TODO: remove hard code, need icons for different states
|
||||
const iconBuffer = fs.readFileSync(path.join(__static, TRAY_ICON_DIR, 'icon.png'));
|
||||
const icon = nativeImage.createFromBuffer(iconBuffer);
|
||||
return icon;
|
||||
}
|
||||
|
||||
function updateTray(status: string): void {
|
||||
// read icon state
|
||||
const current = getIconState();
|
||||
if (current !== status) {
|
||||
setIconState(status);
|
||||
if (status in ICONS) {
|
||||
changeIcon(ICONS[status]);
|
||||
} else {
|
||||
changeIcon(ICONS.default);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function initTray(): void {
|
||||
if (!tray) {
|
||||
try {
|
||||
tray = new Tray(ICONS.default);
|
||||
} catch(e) {
|
||||
console.log('Error initilizing tray', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tray = new Tray(ICON_PATH + "000.png");
|
||||
}
|
||||
|
|
@ -36,7 +36,7 @@ interface Message {
|
|||
|
||||
interface Content {
|
||||
text: string;
|
||||
audio: string | boolean;
|
||||
audio: Int16Array | false;
|
||||
}
|
||||
|
||||
interface Context {
|
||||
|
|
|
|||
|
|
@ -1,95 +1,102 @@
|
|||
import { ipcEmit } from "@/composables/useEmitter";
|
||||
|
||||
class Profile implements Profile{
|
||||
class Profile implements Profile
|
||||
{
|
||||
crimata_id = "";
|
||||
name = "";
|
||||
photo = "";
|
||||
}
|
||||
|
||||
export default class UIState {
|
||||
|
||||
export default class UIState
|
||||
{
|
||||
profile: Profile;
|
||||
messages: Message[];
|
||||
|
||||
constructor() {
|
||||
constructor()
|
||||
{
|
||||
this.profile = new Profile();
|
||||
this.messages = [];
|
||||
}
|
||||
|
||||
emit() {
|
||||
emit()
|
||||
{
|
||||
ipcEmit("set-profile", this.profile);
|
||||
ipcEmit("init-messages", this.messages);
|
||||
}
|
||||
|
||||
set (message: Init) {
|
||||
set (message: Init)
|
||||
{
|
||||
this.profile = message.profile;
|
||||
this.messages = message.messages;
|
||||
this.emit();
|
||||
}
|
||||
|
||||
update(update: Update) {
|
||||
|
||||
if (update.name === "profile") {
|
||||
update(update: Update)
|
||||
{
|
||||
if (update.name === "profile")
|
||||
{
|
||||
this.profile = update.data as Profile;
|
||||
ipcEmit("set-profile", this.profile);
|
||||
}
|
||||
|
||||
else if (update.name === "add") {
|
||||
else if (update.name === "add")
|
||||
{
|
||||
this.messages.push(update.data as Message);
|
||||
ipcEmit("add-message", update.data);
|
||||
}
|
||||
|
||||
else if (update.name === "annotate") {
|
||||
else if (update.name === "annotate")
|
||||
{
|
||||
this._annotate(update.data as Annotation);
|
||||
ipcEmit("update-message", update.data);
|
||||
}
|
||||
|
||||
else {
|
||||
else
|
||||
{
|
||||
this._delete(update.data as string);
|
||||
ipcEmit("delete-message", update.data);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
reset() {
|
||||
reset()
|
||||
{
|
||||
this.profile = new Profile();
|
||||
this.messages = [];
|
||||
this.emit();
|
||||
}
|
||||
|
||||
_annotate(annotation: Annotation) {
|
||||
|
||||
for (const i in this.messages) {
|
||||
|
||||
if (this.messages[i].uid == annotation.uid) {
|
||||
|
||||
if (annotation.name == "content") {
|
||||
_annotate(annotation: Annotation)
|
||||
{
|
||||
for (const i in this.messages)
|
||||
{
|
||||
if (this.messages[i].uid == annotation.uid)
|
||||
{
|
||||
if (annotation.name == "content")
|
||||
{
|
||||
this.messages[i].content = annotation.data as Content[];
|
||||
}
|
||||
|
||||
if (annotation.name == "context") {
|
||||
if (annotation.name == "context")
|
||||
{
|
||||
this.messages[i].context = annotation.data as Context;
|
||||
}
|
||||
|
||||
if (annotation.name == "seen") {
|
||||
if (annotation.name == "seen")
|
||||
{
|
||||
this.messages[i].seen = annotation.data as boolean;
|
||||
}
|
||||
|
||||
ipcEmit("update-message", annotation);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
_delete(uid: string) {
|
||||
for (let i = 0; i < this.messages.length; i++) {
|
||||
if (this.messages[i].uid === uid) {
|
||||
_delete(uid: string)
|
||||
{
|
||||
for (let i = 0; i < this.messages.length; i++)
|
||||
{
|
||||
if (this.messages[i].uid === uid)
|
||||
{
|
||||
this.messages.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
126
yarn.lock
|
|
@ -3478,7 +3478,7 @@ chokidar@^3.0.2, chokidar@^3.3.0, chokidar@^3.4.1:
|
|||
optionalDependencies:
|
||||
fsevents "~2.1.2"
|
||||
|
||||
chownr@^1.0.1, chownr@^1.1.1, chownr@^1.1.2:
|
||||
chownr@^1.1.1, chownr@^1.1.2:
|
||||
version "1.1.4"
|
||||
resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b"
|
||||
integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==
|
||||
|
|
@ -4364,7 +4364,7 @@ debounce-fn@^4.0.0:
|
|||
dependencies:
|
||||
mimic-fn "^3.0.0"
|
||||
|
||||
debug@2.6.9, debug@^2.1.3, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9:
|
||||
debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9:
|
||||
version "2.6.9"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
|
||||
integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
|
||||
|
|
@ -6807,16 +6807,6 @@ invariant@^2.2.2, invariant@^2.2.4:
|
|||
dependencies:
|
||||
loose-envify "^1.0.0"
|
||||
|
||||
iohook@^0.9.3:
|
||||
version "0.9.3"
|
||||
resolved "https://registry.yarnpkg.com/iohook/-/iohook-0.9.3.tgz#3beae68daeac09bad3a7fde290f595ac7172b6e7"
|
||||
integrity sha512-O6xHAyRfJvVUYu87sZH5CgBPLo/f70NaZSIbm3o6U4L/I9PdntfjlNYrJ7hbv5nPUwfE3k5TGDH1R1bFVdDBZg==
|
||||
dependencies:
|
||||
nugget "^2.0.1"
|
||||
pump "^1.0.3"
|
||||
rc "^1.2.8"
|
||||
tar-fs "^1.16.3"
|
||||
|
||||
ip-regex@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9"
|
||||
|
|
@ -7204,11 +7194,6 @@ is-yarn-global@^0.3.0:
|
|||
resolved "https://registry.yarnpkg.com/is-yarn-global/-/is-yarn-global-0.3.0.tgz#d502d3382590ea3004893746754c89139973e232"
|
||||
integrity sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==
|
||||
|
||||
isarray@0.0.1:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
|
||||
integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=
|
||||
|
||||
isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
|
||||
|
|
@ -8444,7 +8429,7 @@ memory-fs@^0.5.0:
|
|||
errno "^0.1.3"
|
||||
readable-stream "^2.0.1"
|
||||
|
||||
meow@^3.1.0, meow@^3.7.0:
|
||||
meow@^3.7.0:
|
||||
version "3.7.0"
|
||||
resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb"
|
||||
integrity sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=
|
||||
|
|
@ -8601,7 +8586,7 @@ minimatch@^3.0.4, minimatch@~3.0.2:
|
|||
dependencies:
|
||||
brace-expansion "^1.1.7"
|
||||
|
||||
minimist@^1.1.0, minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.5:
|
||||
minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.5:
|
||||
version "1.2.5"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
|
||||
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
|
||||
|
|
@ -9041,19 +9026,6 @@ nth-check@^1.0.2, nth-check@~1.0.1:
|
|||
dependencies:
|
||||
boolbase "~1.0.0"
|
||||
|
||||
nugget@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/nugget/-/nugget-2.0.1.tgz#201095a487e1ad36081b3432fa3cada4f8d071b0"
|
||||
integrity sha1-IBCVpIfhrTYIGzQy+jytpPjQcbA=
|
||||
dependencies:
|
||||
debug "^2.1.3"
|
||||
minimist "^1.1.0"
|
||||
pretty-bytes "^1.0.2"
|
||||
progress-stream "^1.1.0"
|
||||
request "^2.45.0"
|
||||
single-line-log "^1.1.2"
|
||||
throttleit "0.0.2"
|
||||
|
||||
num2fraction@^1.2.2:
|
||||
version "1.2.2"
|
||||
resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede"
|
||||
|
|
@ -9111,11 +9083,6 @@ object-keys@^1.0.12, object-keys@^1.1.1:
|
|||
resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
|
||||
integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
|
||||
|
||||
object-keys@~0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-0.4.0.tgz#28a6aae7428dd2c3a92f3d95f21335dd204e0336"
|
||||
integrity sha1-KKaq50KN0sOpLz2V8hM13SBOAzY=
|
||||
|
||||
object-visit@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb"
|
||||
|
|
@ -10047,14 +10014,6 @@ prettier@^1.18.2:
|
|||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb"
|
||||
integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==
|
||||
|
||||
pretty-bytes@^1.0.2:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-1.0.4.tgz#0a22e8210609ad35542f8c8d5d2159aff0751c84"
|
||||
integrity sha1-CiLoIQYJrTVUL4yNXSFZr/B1HIQ=
|
||||
dependencies:
|
||||
get-stdin "^4.0.1"
|
||||
meow "^3.1.0"
|
||||
|
||||
pretty-error@^2.0.2:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-2.1.2.tgz#be89f82d81b1c86ec8fdfbc385045882727f93b6"
|
||||
|
|
@ -10092,14 +10051,6 @@ process@^0.11.10:
|
|||
resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
|
||||
integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI=
|
||||
|
||||
progress-stream@^1.1.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/progress-stream/-/progress-stream-1.2.0.tgz#2cd3cfea33ba3a89c9c121ec3347abe9ab125f77"
|
||||
integrity sha1-LNPP6jO6OonJwSHsM0er6asSX3c=
|
||||
dependencies:
|
||||
speedometer "~0.1.2"
|
||||
through2 "~0.2.3"
|
||||
|
||||
progress@^2.0.0, progress@^2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
|
||||
|
|
@ -10158,14 +10109,6 @@ public-encrypt@^4.0.0:
|
|||
randombytes "^2.0.1"
|
||||
safe-buffer "^5.1.2"
|
||||
|
||||
pump@^1.0.0, pump@^1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/pump/-/pump-1.0.3.tgz#5dfe8311c33bbf6fc18261f9f34702c47c08a954"
|
||||
integrity sha512-8k0JupWme55+9tCVE+FS5ULT3K6AbgqrGa58lTT49RpyfwwcGedHqaC5LlQNdEAumn/wFsu6aPwkuPMioy8kqw==
|
||||
dependencies:
|
||||
end-of-stream "^1.1.0"
|
||||
once "^1.3.1"
|
||||
|
||||
pump@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909"
|
||||
|
|
@ -10382,16 +10325,6 @@ readable-stream@^3.0.0, readable-stream@^3.0.6, readable-stream@^3.1.1, readable
|
|||
string_decoder "^1.1.1"
|
||||
util-deprecate "^1.0.1"
|
||||
|
||||
readable-stream@~1.1.9:
|
||||
version "1.1.14"
|
||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9"
|
||||
integrity sha1-fPTFTvZI44EwhMY23SB54WbAgdk=
|
||||
dependencies:
|
||||
core-util-is "~1.0.0"
|
||||
inherits "~2.0.1"
|
||||
isarray "0.0.1"
|
||||
string_decoder "~0.10.x"
|
||||
|
||||
readdirp@^2.2.1:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525"
|
||||
|
|
@ -10570,7 +10503,7 @@ request-promise-native@^1.0.5, request-promise-native@^1.0.7:
|
|||
stealthy-require "^1.1.1"
|
||||
tough-cookie "^2.3.3"
|
||||
|
||||
request@^2.45.0, request@^2.83.0, request@^2.87.0, request@^2.88.0, request@^2.88.2:
|
||||
request@^2.83.0, request@^2.87.0, request@^2.88.0, request@^2.88.2:
|
||||
version "2.88.2"
|
||||
resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3"
|
||||
integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==
|
||||
|
|
@ -11116,13 +11049,6 @@ simple-swizzle@^0.2.2:
|
|||
dependencies:
|
||||
is-arrayish "^0.3.1"
|
||||
|
||||
single-line-log@^1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/single-line-log/-/single-line-log-1.1.2.tgz#c2f83f273a3e1a16edb0995661da0ed5ef033364"
|
||||
integrity sha1-wvg/Jzo+GhbtsJlWYdoO1e8DM2Q=
|
||||
dependencies:
|
||||
string-width "^1.0.1"
|
||||
|
||||
sisteransi@^1.0.5:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed"
|
||||
|
|
@ -11339,11 +11265,6 @@ spectron@11.0.0:
|
|||
split "^1.0.0"
|
||||
webdriverio "^4.13.0"
|
||||
|
||||
speedometer@~0.1.2:
|
||||
version "0.1.4"
|
||||
resolved "https://registry.yarnpkg.com/speedometer/-/speedometer-0.1.4.tgz#9876dbd2a169d3115402d48e6ea6329c8816a50d"
|
||||
integrity sha1-mHbb0qFp0xFUAtSObqYynIgWpQ0=
|
||||
|
||||
split-string@^3.0.1, split-string@^3.0.2:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2"
|
||||
|
|
@ -11578,11 +11499,6 @@ string_decoder@^1.0.0, string_decoder@^1.1.1:
|
|||
dependencies:
|
||||
safe-buffer "~5.2.0"
|
||||
|
||||
string_decoder@~0.10.x:
|
||||
version "0.10.31"
|
||||
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
|
||||
integrity sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=
|
||||
|
||||
string_decoder@~1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
|
||||
|
|
@ -11769,17 +11685,7 @@ tapable@^1.0.0, tapable@^1.1.3:
|
|||
resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2"
|
||||
integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==
|
||||
|
||||
tar-fs@^1.16.3:
|
||||
version "1.16.3"
|
||||
resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-1.16.3.tgz#966a628841da2c4010406a82167cbd5e0c72d509"
|
||||
integrity sha512-NvCeXpYx7OsmOh8zIOP/ebG55zZmxLE0etfWRbWok+q2Qo8x/vOR/IJT1taADXPe+jsiu9axDb3X4B+iIgNlKw==
|
||||
dependencies:
|
||||
chownr "^1.0.1"
|
||||
mkdirp "^0.5.1"
|
||||
pump "^1.0.0"
|
||||
tar-stream "^1.1.2"
|
||||
|
||||
tar-stream@^1.1.2, tar-stream@^1.5.0:
|
||||
tar-stream@^1.5.0:
|
||||
version "1.6.2"
|
||||
resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-1.6.2.tgz#8ea55dab37972253d9a9af90fdcd559ae435c555"
|
||||
integrity sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==
|
||||
|
|
@ -11923,11 +11829,6 @@ throat@^4.0.0:
|
|||
resolved "https://registry.yarnpkg.com/throat/-/throat-4.1.0.tgz#89037cbc92c56ab18926e6ba4cbb200e15672a6a"
|
||||
integrity sha1-iQN8vJLFarGJJua6TLsgDhVnKmo=
|
||||
|
||||
throttleit@0.0.2:
|
||||
version "0.0.2"
|
||||
resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-0.0.2.tgz#cfedf88e60c00dd9697b61fdd2a8343a9b680eaf"
|
||||
integrity sha1-z+34jmDADdlpe2H90qg0OptoDq8=
|
||||
|
||||
through2-filter@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/through2-filter/-/through2-filter-3.0.0.tgz#700e786df2367c2c88cd8aa5be4cf9c1e7831254"
|
||||
|
|
@ -11952,14 +11853,6 @@ through2@^2.0.0, through2@~2.0.0:
|
|||
readable-stream "~2.3.6"
|
||||
xtend "~4.0.1"
|
||||
|
||||
through2@~0.2.3:
|
||||
version "0.2.3"
|
||||
resolved "https://registry.yarnpkg.com/through2/-/through2-0.2.3.tgz#eb3284da4ea311b6cc8ace3653748a52abf25a3f"
|
||||
integrity sha1-6zKE2k6jEbbMis42U3SKUqvyWj8=
|
||||
dependencies:
|
||||
readable-stream "~1.1.9"
|
||||
xtend "~2.1.1"
|
||||
|
||||
through@2, through@^2.3.6:
|
||||
version "2.3.8"
|
||||
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
|
||||
|
|
@ -13141,13 +13034,6 @@ xtend@^4.0.0, xtend@~4.0.0, xtend@~4.0.1:
|
|||
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
|
||||
integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
|
||||
|
||||
xtend@~2.1.1:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/xtend/-/xtend-2.1.2.tgz#6efecc2a4dad8e6962c4901b337ce7ba87b5d28b"
|
||||
integrity sha1-bv7MKk2tjmlixJAbM3znuoe10os=
|
||||
dependencies:
|
||||
object-keys "~0.4.0"
|
||||
|
||||
y18n@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b"
|
||||
|
|
|
|||