Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
95e3dab1c2 | ||
|
|
2c510765c0 | ||
|
|
56196bcbcf | ||
|
|
c8411d573a | ||
|
|
a89e856661 | ||
|
|
ab487e4f5e | ||
|
|
a22b3c68e5 | ||
|
|
a6351af83c | ||
|
|
798b848c9b | ||
|
|
d963f7d8a8 | ||
|
|
0a3c9f71d8 | ||
|
|
d851db2fcc | ||
|
|
9e8641b4de | ||
|
|
3b5da6124f | ||
|
|
628210dab5 | ||
|
|
25b03d501c | ||
|
|
0cfa2340be | ||
|
|
0079c72ea0 | ||
|
|
ac8f338d75 | ||
|
|
7dbf4e6aa6 | ||
|
|
716730fdd2 | ||
|
|
87dce82c53 | ||
|
|
ddb2f4df0f | ||
|
|
17e4b06948 | ||
|
|
69dc3177ee | ||
|
|
6db693f9d3 |
70 changed files with 2345 additions and 2584 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -9,6 +9,7 @@ crash.log
|
||||||
# local env files
|
# local env files
|
||||||
.env.local
|
.env.local
|
||||||
.env.*.local
|
.env.*.local
|
||||||
|
.env
|
||||||
|
|
||||||
# Log files
|
# Log files
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
|
|
|
||||||
|
|
@ -1,79 +0,0 @@
|
||||||
# Do not edit. File was generated by node-gyp's "configure" step
|
|
||||||
{
|
|
||||||
"target_defaults": {
|
|
||||||
"cflags": [],
|
|
||||||
"default_configuration": "Release",
|
|
||||||
"defines": [],
|
|
||||||
"include_dirs": [],
|
|
||||||
"libraries": []
|
|
||||||
},
|
|
||||||
"variables": {
|
|
||||||
"asan": 0,
|
|
||||||
"build_v8_with_gn": "false",
|
|
||||||
"coverage": "false",
|
|
||||||
"dcheck_always_on": 0,
|
|
||||||
"debug_nghttp2": "false",
|
|
||||||
"debug_node": "false",
|
|
||||||
"enable_lto": "false",
|
|
||||||
"enable_pgo_generate": "false",
|
|
||||||
"enable_pgo_use": "false",
|
|
||||||
"error_on_warn": "false",
|
|
||||||
"force_dynamic_crt": 0,
|
|
||||||
"host_arch": "x64",
|
|
||||||
"icu_data_in": "../../deps/icu-tmp/icudt67l.dat",
|
|
||||||
"icu_endianness": "l",
|
|
||||||
"icu_gyp_path": "tools/icu/icu-generic.gyp",
|
|
||||||
"icu_path": "deps/icu-small",
|
|
||||||
"icu_small": "false",
|
|
||||||
"icu_ver_major": "67",
|
|
||||||
"is_debug": 0,
|
|
||||||
"llvm_version": "0.0",
|
|
||||||
"napi_build_version": "6",
|
|
||||||
"node_byteorder": "little",
|
|
||||||
"node_debug_lib": "false",
|
|
||||||
"node_enable_d8": "false",
|
|
||||||
"node_install_npm": "true",
|
|
||||||
"node_module_version": 83,
|
|
||||||
"node_no_browser_globals": "false",
|
|
||||||
"node_prefix": "/",
|
|
||||||
"node_release_urlbase": "https://nodejs.org/download/release/",
|
|
||||||
"node_shared": "false",
|
|
||||||
"node_shared_brotli": "false",
|
|
||||||
"node_shared_cares": "false",
|
|
||||||
"node_shared_http_parser": "false",
|
|
||||||
"node_shared_libuv": "false",
|
|
||||||
"node_shared_nghttp2": "false",
|
|
||||||
"node_shared_openssl": "false",
|
|
||||||
"node_shared_zlib": "false",
|
|
||||||
"node_tag": "",
|
|
||||||
"node_target_type": "executable",
|
|
||||||
"node_use_bundled_v8": "true",
|
|
||||||
"node_use_dtrace": "true",
|
|
||||||
"node_use_etw": "false",
|
|
||||||
"node_use_node_code_cache": "true",
|
|
||||||
"node_use_node_snapshot": "true",
|
|
||||||
"node_use_openssl": "true",
|
|
||||||
"node_use_v8_platform": "true",
|
|
||||||
"node_with_ltcg": "false",
|
|
||||||
"node_without_node_options": "false",
|
|
||||||
"openssl_fips": "",
|
|
||||||
"openssl_is_fips": "false",
|
|
||||||
"shlib_suffix": "83.dylib",
|
|
||||||
"target_arch": "x64",
|
|
||||||
"v8_enable_31bit_smis_on_64bit_arch": 0,
|
|
||||||
"v8_enable_gdbjit": 0,
|
|
||||||
"v8_enable_i18n_support": 1,
|
|
||||||
"v8_enable_inspector": 1,
|
|
||||||
"v8_enable_pointer_compression": 0,
|
|
||||||
"v8_no_strict_aliasing": 1,
|
|
||||||
"v8_optimized_debug": 1,
|
|
||||||
"v8_promise_internal_field_count": 1,
|
|
||||||
"v8_random_seed": 0,
|
|
||||||
"v8_trace_maps": 0,
|
|
||||||
"v8_use_siphash": 1,
|
|
||||||
"want_separate_host_toolset": 0,
|
|
||||||
"xcode_version": "11.0",
|
|
||||||
"nodedir": "/Users/Enrique/Library/Caches/node-gyp/14.4.0",
|
|
||||||
"standalone_static_library": 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
11
package.json
11
package.json
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "Crimata",
|
"name": "Crimata",
|
||||||
"version": "0.9.8",
|
"version": "0.9.9",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Cross-platform messenger application built with electron, vue3, and TS.",
|
"description": "Cross-platform messenger application built with electron, vue3, and TS.",
|
||||||
"author": {
|
"author": {
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
"postinstall": "electron-builder install-app-deps",
|
"postinstall": "electron-builder install-app-deps",
|
||||||
"postuninstall": "electron-builder install-app-deps"
|
"postuninstall": "electron-builder install-app-deps"
|
||||||
},
|
},
|
||||||
"main": "background.js",
|
"main": "init.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@google-cloud/speech": "^4.2.0",
|
"@google-cloud/speech": "^4.2.0",
|
||||||
"@types/animejs": "^3.1.2",
|
"@types/animejs": "^3.1.2",
|
||||||
|
|
@ -22,8 +22,10 @@
|
||||||
"@types/uuid": "^8.3.0",
|
"@types/uuid": "^8.3.0",
|
||||||
"@types/ws": "^7.2.7",
|
"@types/ws": "^7.2.7",
|
||||||
"animejs": "^3.2.0",
|
"animejs": "^3.2.0",
|
||||||
|
"axios": "^0.21.1",
|
||||||
"core-js": "^3.6.5",
|
"core-js": "^3.6.5",
|
||||||
"electron-is-dev": "^2.0.0",
|
"electron-is-dev": "^2.0.0",
|
||||||
|
"electron-store": "^8.0.0",
|
||||||
"electron-updater": "^4.3.8",
|
"electron-updater": "^4.3.8",
|
||||||
"mitt": "^2.1.0",
|
"mitt": "^2.1.0",
|
||||||
"naudiodon": "^2.3.2",
|
"naudiodon": "^2.3.2",
|
||||||
|
|
@ -36,6 +38,7 @@
|
||||||
"ws": "^7.3.1"
|
"ws": "^7.3.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/axios": "^0.14.0",
|
||||||
"@types/electron-devtools-installer": "^2.2.0",
|
"@types/electron-devtools-installer": "^2.2.0",
|
||||||
"@types/jest": "^24.0.19",
|
"@types/jest": "^24.0.19",
|
||||||
"@typescript-eslint/eslint-plugin": "^2.33.0",
|
"@typescript-eslint/eslint-plugin": "^2.33.0",
|
||||||
|
|
@ -69,7 +72,9 @@
|
||||||
"lintOnSave": false,
|
"lintOnSave": false,
|
||||||
"pluginOptions": {
|
"pluginOptions": {
|
||||||
"electronBuilder": {
|
"electronBuilder": {
|
||||||
"preload": "src/preload.ts",
|
"mainProcessFile": "./src/init.ts",
|
||||||
|
"rendererProcessFile": "./src/render/main.ts",
|
||||||
|
"preload": "./src/render/preload.ts",
|
||||||
"builderOptions": {
|
"builderOptions": {
|
||||||
"appId": "com.crimata.ElectronUpdaterApp",
|
"appId": "com.crimata.ElectronUpdaterApp",
|
||||||
"artifactName": "${productName}-${version}.${ext}",
|
"artifactName": "${productName}-${version}.${ext}",
|
||||||
|
|
|
||||||
|
|
@ -11,10 +11,6 @@
|
||||||
<noscript>
|
<noscript>
|
||||||
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||||
</noscript>
|
</noscript>
|
||||||
<!-- <h1>Hello World!</h1>
|
|
||||||
We are using Node.js <span id="node-version"></span>, Chromium
|
|
||||||
<span id="chrome-version"></span>, and Electron
|
|
||||||
<span id="electron-version"></span>. -->
|
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<!-- built files will be auto injected -->
|
<!-- built files will be auto injected -->
|
||||||
</body>
|
</body>
|
||||||
|
|
|
||||||
183
src/App.vue
183
src/App.vue
|
|
@ -1,183 +0,0 @@
|
||||||
<template>
|
|
||||||
<!-- Only render when profile has been set -->
|
|
||||||
<div id="app" v-if="(ready)">
|
|
||||||
|
|
||||||
<button id="titlebar" />
|
|
||||||
|
|
||||||
<!-- Minimize and exit buttons. -->
|
|
||||||
<span class="menu">
|
|
||||||
<button
|
|
||||||
class="menuButton exitButton"
|
|
||||||
@click.prevent="post('nav-bar', 'close')"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
class="menuButton minimizeButton"
|
|
||||||
@click.prevent="post('nav-bar', 'min')"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<!-- Main Pages -->
|
|
||||||
<Messenger
|
|
||||||
v-if="profile"
|
|
||||||
:profile="profile"
|
|
||||||
:newMessages="newMessages"
|
|
||||||
/>
|
|
||||||
<Login v-else />
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Show spash screen if ready is false -->
|
|
||||||
<Splash v-else />
|
|
||||||
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent, onMounted, onUnmounted, ref } from "vue";
|
|
||||||
import { IpcRendererEvent } from "electron";
|
|
||||||
import { useIpc } from "@/modules/ipc";
|
|
||||||
|
|
||||||
import Splash from "@/components/splash.vue";
|
|
||||||
import Messenger from "@/components/messenger.vue";
|
|
||||||
import Login from "@/components/login.vue";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
components: {
|
|
||||||
Splash,
|
|
||||||
Messenger,
|
|
||||||
Login
|
|
||||||
},
|
|
||||||
|
|
||||||
setup() {
|
|
||||||
const { post } = useIpc();
|
|
||||||
|
|
||||||
// Whether browser has received user info yet.
|
|
||||||
const ready = ref(false);
|
|
||||||
|
|
||||||
// Information about current user.
|
|
||||||
const profile = ref(false);
|
|
||||||
|
|
||||||
// New messages that browser missed while closed.
|
|
||||||
const newMessages = ref([])
|
|
||||||
|
|
||||||
// Receive updated information about the session.
|
|
||||||
const updateState = (_event: IpcRendererEvent, payload: any) => {
|
|
||||||
console.log("APP:Received updated profile and new messages: \n" +
|
|
||||||
` profile: ${payload.message.profile}\n` +
|
|
||||||
` new: ${payload.message.newMessages}`)
|
|
||||||
|
|
||||||
if (payload.message.profile) {
|
|
||||||
console.log(`APP:Logged-in, showing Messenger View.`)
|
|
||||||
} else {
|
|
||||||
console.log(`APP:Logged-out, showing Login View.`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set profile and newMessages.
|
|
||||||
profile.value = payload.message.profile;
|
|
||||||
newMessages.value = payload.message.newMessages;
|
|
||||||
|
|
||||||
ready.value = true;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
console.log("APP:mounted.")
|
|
||||||
window.ipcRenderer.on("update-state", updateState)
|
|
||||||
window.ipcRenderer.on("update_available", () => {
|
|
||||||
console.log('testing auto update');
|
|
||||||
})
|
|
||||||
|
|
||||||
window.ipcRenderer.on("update_downloaded", () => {
|
|
||||||
console.log('testing update download');
|
|
||||||
})
|
|
||||||
post("app-mounted", "")
|
|
||||||
});
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
window.ipcRenderer.removeAllListeners("update-state")
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
ready,
|
|
||||||
profile,
|
|
||||||
post,
|
|
||||||
newMessages
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
|
|
||||||
html, body {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
// Background color set in window.ts
|
|
||||||
}
|
|
||||||
|
|
||||||
#app {
|
|
||||||
font-family: "SF Pro Text";
|
|
||||||
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
|
|
||||||
height: 100vh;
|
|
||||||
width: 100vw;
|
|
||||||
|
|
||||||
border-radius: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#titlebar {
|
|
||||||
position: fixed;
|
|
||||||
|
|
||||||
width: 100vw;
|
|
||||||
height: 54px;
|
|
||||||
|
|
||||||
opacity: 0.75;
|
|
||||||
|
|
||||||
background-color: #EBEBEB;
|
|
||||||
|
|
||||||
-webkit-app-region: drag;
|
|
||||||
|
|
||||||
border: none;
|
|
||||||
outline: none;
|
|
||||||
|
|
||||||
z-index: 1;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu {
|
|
||||||
position: fixed;
|
|
||||||
margin-left: 20px;
|
|
||||||
margin-top: 20px;
|
|
||||||
|
|
||||||
z-index: 2;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.menuButton {
|
|
||||||
border: none;
|
|
||||||
outline:none;
|
|
||||||
min-width: 14px;
|
|
||||||
min-height: 14px;
|
|
||||||
border-radius: 7px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.exitButton {
|
|
||||||
background-color: #FF6157;
|
|
||||||
}
|
|
||||||
|
|
||||||
.exitButton:active {
|
|
||||||
background: #c14645;
|
|
||||||
}
|
|
||||||
|
|
||||||
.minimizeButton {
|
|
||||||
background-color: #FFC12F;
|
|
||||||
margin-left: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.minimizeButton:active {
|
|
||||||
background-color: #c08e38;
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
||||||
95
src/account.ts
Normal file
95
src/account.ts
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
|
||||||
|
import { postAuth, postLogin, postLogout } from "@/api/account";
|
||||||
|
import { endSession, launchSession } from "@/session";
|
||||||
|
import { getToken, setToken, setProfile, getProfile, clearStore } from "./store";
|
||||||
|
import { parseAuthRes } from "./auth";
|
||||||
|
import { ipcEmit } from "@/composables/useEmitter";
|
||||||
|
|
||||||
|
export const accountAuth = async (): Promise<Error | AuthState> => {
|
||||||
|
|
||||||
|
/* attempt to get a login token from the store */
|
||||||
|
const token = getToken();
|
||||||
|
|
||||||
|
/* try to login with it, returns platform secret and new token on success */
|
||||||
|
if (token) {
|
||||||
|
try {
|
||||||
|
|
||||||
|
const res = await postAuth(token);
|
||||||
|
|
||||||
|
const parsed = parseAuthRes(res);
|
||||||
|
|
||||||
|
setToken(parsed.token)
|
||||||
|
setProfile(parsed.profile);
|
||||||
|
|
||||||
|
return {
|
||||||
|
profile: parsed.profile,
|
||||||
|
token: parsed.token
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch(e) {
|
||||||
|
console.log('[ACCOUNT]', e);
|
||||||
|
clearStore();
|
||||||
|
throw(new Error('Failed to authenticate.'));
|
||||||
|
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw(new Error('Unable to authenticate.'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const accountLogin: IpcHandlerCallback<AccountCredentials, Profile> = async (payload) => {
|
||||||
|
const account = payload as AccountCredentials;
|
||||||
|
try {
|
||||||
|
|
||||||
|
// attempt login with email password
|
||||||
|
const res = await postLogin(account.email, account.password);
|
||||||
|
const parsed = parseAuthRes(res);
|
||||||
|
|
||||||
|
// save jwt token and profile
|
||||||
|
setToken(parsed.token);
|
||||||
|
setProfile(parsed.profile);
|
||||||
|
|
||||||
|
// launch session
|
||||||
|
launchSession(parsed.token);
|
||||||
|
|
||||||
|
// return profile to renderer
|
||||||
|
return parsed.profile;
|
||||||
|
|
||||||
|
} catch(e) {
|
||||||
|
clearStore();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const accountLogout = async (): Promise<Error | void> => {
|
||||||
|
|
||||||
|
try {
|
||||||
|
// post logout to backend
|
||||||
|
await postLogout();
|
||||||
|
|
||||||
|
// remove key and crimataId
|
||||||
|
clearStore();
|
||||||
|
|
||||||
|
// kill crimata platform session
|
||||||
|
endSession();
|
||||||
|
|
||||||
|
return;
|
||||||
|
|
||||||
|
} catch(e) {
|
||||||
|
console.log('[ACCOUNT]', e);
|
||||||
|
return (new Error('Failed to logout. Please try again.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateAppState = (): void => {
|
||||||
|
|
||||||
|
const profile = getProfile();
|
||||||
|
|
||||||
|
ipcEmit("set-profile", profile);
|
||||||
|
|
||||||
|
// ipcEmit('messages') etc
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
31
src/api/account.ts
Normal file
31
src/api/account.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
|
||||||
|
import useHttp from "@/composables/useHttp";
|
||||||
|
import axios from "axios";
|
||||||
|
import {config} from "@/config";
|
||||||
|
|
||||||
|
const { post } = useHttp();
|
||||||
|
|
||||||
|
export const postAuth = async (token: string) => (
|
||||||
|
await axios({
|
||||||
|
url: config.BUSINESS_URL + config.BUSINESS_PREFIX + '/account/authenticate',
|
||||||
|
headers: {
|
||||||
|
Cookie: `crimataCookie=${token}`
|
||||||
|
},
|
||||||
|
method: 'POST',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
export const postLogin = async (email: string, password: string) => (
|
||||||
|
await post('/account/login', { email, password })
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
export const postLogout =
|
||||||
|
async (): Promise<null | Error> => (await post('/account/logout'));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="13.795" height="12.759" viewBox="0 0 13.795 12.759">
|
|
||||||
<g id="Group_544" data-name="Group 544" 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 6.627)">
|
|
||||||
<circle id="Ellipse_50" data-name="Ellipse 50" cx="3.066" cy="3.066" r="3.066" transform="translate(0 0)" fill="#383838"/>
|
|
||||||
<circle id="Ellipse_51" data-name="Ellipse 51" cx="3.066" cy="3.066" r="3.066" transform="translate(7.664 0)" fill="#383838"/>
|
|
||||||
</g>
|
|
||||||
<circle id="Ellipse_52" data-name="Ellipse 52" cx="3.066" cy="3.066" r="3.066" transform="translate(3.826 0)" fill="#383838"/>
|
|
||||||
<path id="Path_150" data-name="Path 150" d="M36.27,83.67" transform="translate(-33.199 -73.977)" fill="#ff0"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 973 B |
187
src/audio.ts
Normal file
187
src/audio.ts
Normal file
|
|
@ -0,0 +1,187 @@
|
||||||
|
/* eslint @typescript-eslint/no-var-requires: "off" */
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
// where the audio goes
|
||||||
|
let buffer: ArrayBuffer[] = [];
|
||||||
|
|
||||||
|
// place audio data in buffer
|
||||||
|
export const collect: IpcListenerCallback<ArrayBuffer> = (chunk) => {
|
||||||
|
if (chunk) buffer.push(chunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
// return audio and clear buffer
|
||||||
|
export const flush = async () => {
|
||||||
|
const bufferCopy = buffer;
|
||||||
|
buffer = [];
|
||||||
|
return bufferCopy;
|
||||||
|
}
|
||||||
|
|
||||||
|
// import { backgroundMitt } from '@/modules/emitter';
|
||||||
|
// const portAudio = require('naudiodon');
|
||||||
|
|
||||||
|
// // Audio in and out stream objects.
|
||||||
|
// let ai: typeof portAudio.AudioIO | boolean = false;
|
||||||
|
// let ao: typeof portAudio.AudioIO | boolean = false;
|
||||||
|
|
||||||
|
// // Whether activly recording.
|
||||||
|
// let record = false;
|
||||||
|
|
||||||
|
// const audioContainer = {
|
||||||
|
// input: '',
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const audioOptions = {
|
||||||
|
// channelCount: 1,
|
||||||
|
// sampleFormat: 16,
|
||||||
|
// sampleRate: 16000,
|
||||||
|
// deviceId: -1,
|
||||||
|
// closeOnError: false,
|
||||||
|
// }
|
||||||
|
|
||||||
|
// export const toggleRecord = (): void => { record = !record };
|
||||||
|
|
||||||
|
|
||||||
|
// export const fetchAudioInput = (): Promise<Error | string> => (
|
||||||
|
|
||||||
|
// new Promise((resolve, reject) => {
|
||||||
|
|
||||||
|
// try {
|
||||||
|
// resolve(audioContainer.input);
|
||||||
|
// toggleRecord();
|
||||||
|
// } catch (e) {
|
||||||
|
// reject(new Error('Failed to fetch the audio.'))
|
||||||
|
// }
|
||||||
|
|
||||||
|
// })
|
||||||
|
// )
|
||||||
|
|
||||||
|
|
||||||
|
// // Main audio function run by run.ts module.
|
||||||
|
// export function initAudioIO(): void {
|
||||||
|
// console.log("AUDIO:Starting io streams.")
|
||||||
|
|
||||||
|
// if (!ai) {
|
||||||
|
|
||||||
|
// // Initialize and start input stream.
|
||||||
|
// ai = new portAudio.AudioIO({ inOptions: audioOptions });
|
||||||
|
// ai.setEncoding("hex");
|
||||||
|
// ai.start();
|
||||||
|
|
||||||
|
// // On each data chunk...
|
||||||
|
// ai.on('data', (chunk: string) => {
|
||||||
|
|
||||||
|
// // If recording, we capture the data.
|
||||||
|
// if (record) {
|
||||||
|
// console.log('AUDIO:Recording...')
|
||||||
|
// audioContainer.input += chunk;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Else, we don't capture and also clear audioContainer.
|
||||||
|
// else {
|
||||||
|
// if (audioContainer.input.length) {
|
||||||
|
// audioContainer.input = "";
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (!ao) {
|
||||||
|
|
||||||
|
// // Initialize and start input stream.
|
||||||
|
// ao = new portAudio.AudioIO({ outOptions: audioOptions });
|
||||||
|
// ao.start();
|
||||||
|
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
// // ---Audio playback--------------------------------------------
|
||||||
|
|
||||||
|
// // Split Buffer into an array of len-sized Buffers.
|
||||||
|
// function bufSplit(buf: Buffer, len: number): Array<Buffer> {
|
||||||
|
// const chunks = [];
|
||||||
|
// let i = 0;
|
||||||
|
// let L = len;
|
||||||
|
|
||||||
|
// while(i < buf.byteLength) {
|
||||||
|
// chunks.push(buf.slice(i, L));
|
||||||
|
// i = L;
|
||||||
|
// L += len;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return chunks;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Audio playback.
|
||||||
|
// export function play(input: string): void {
|
||||||
|
|
||||||
|
// // Format the audio.
|
||||||
|
// const audio = bufSplit(
|
||||||
|
// Buffer.from(input as string, 'hex'),
|
||||||
|
// 8192
|
||||||
|
// );
|
||||||
|
|
||||||
|
// // Called on end of write.
|
||||||
|
// const callback = () => {
|
||||||
|
|
||||||
|
// // We stop audio playback anim.
|
||||||
|
// backgroundMitt.emit('ipc-renderer', {
|
||||||
|
// endpoint: 'stop-playback-anim'
|
||||||
|
// });
|
||||||
|
|
||||||
|
// }
|
||||||
|
|
||||||
|
// write();
|
||||||
|
|
||||||
|
// // Iterate through audio array and write buffers to portAudio writable.
|
||||||
|
// function write() {
|
||||||
|
// let chunk: Buffer;
|
||||||
|
// let ok = true;
|
||||||
|
// let i = 0;
|
||||||
|
|
||||||
|
// do {
|
||||||
|
// chunk = audio[i];
|
||||||
|
// if (i === audio.length - 1) {
|
||||||
|
// // write last chunk.
|
||||||
|
// ao.write(chunk, null, callback);
|
||||||
|
// } else {
|
||||||
|
// // check for backpreassure.
|
||||||
|
// ok = ao.write(chunk, null);
|
||||||
|
// }
|
||||||
|
// i++;
|
||||||
|
// } while (i < audio.length && ok);
|
||||||
|
|
||||||
|
// if (i < audio.length) {
|
||||||
|
// // Had to stop early!
|
||||||
|
// // Write some more once it drains.
|
||||||
|
// ao.once('drain', write);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // -------------------------------------------------------------
|
||||||
|
|
||||||
|
// // Get's called on window close.
|
||||||
|
// export async function stopStream() {
|
||||||
|
// console.log("AUDIO:Stopping audio stream.")
|
||||||
|
// if (ai) {
|
||||||
|
// try {
|
||||||
|
// await ai.quit()
|
||||||
|
// } catch(e){
|
||||||
|
// console.log('AUDIO: Failed to shutdown audio input.');
|
||||||
|
// throw e;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// if (ao) {
|
||||||
|
// try {
|
||||||
|
// await ao.quit()
|
||||||
|
// } catch(e){
|
||||||
|
// console.log('AUDIO: Failed to shutdown audio output.');
|
||||||
|
// throw e;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
13
src/auth.ts
Normal file
13
src/auth.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
|
||||||
|
export const parseAuthRes = (authRes: any) => {
|
||||||
|
const token = authRes.headers['set-cookie'][0].split(";")[0].split("=")[1] as string;
|
||||||
|
const profile = authRes.data as Profile;
|
||||||
|
return {
|
||||||
|
token,
|
||||||
|
profile
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
/*
|
|
||||||
* Entry point for Crimata electron app.
|
|
||||||
* "Look on my Works, ye Mighty, and despair!"
|
|
||||||
*/
|
|
||||||
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
import { initApp } from './background/init';
|
|
||||||
import { protocol } from "electron";
|
|
||||||
|
|
||||||
// Scheme must be registered before the app is ready
|
|
||||||
protocol.registerSchemesAsPrivileged([
|
|
||||||
{ scheme: "app", privileges: { secure: true, standard: true } }
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Load environment variable
|
|
||||||
const isDev = require('electron-is-dev');
|
|
||||||
|
|
||||||
// NOTE Program Begins Here
|
|
||||||
(async () => {
|
|
||||||
|
|
||||||
console.log('Starting Crimata electron app.');
|
|
||||||
await initApp(isDev);
|
|
||||||
|
|
||||||
})();
|
|
||||||
|
|
@ -1,188 +0,0 @@
|
||||||
/* eslint @typescript-eslint/no-var-requires: "off" */
|
|
||||||
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
import { ipcMain } from "electron";
|
|
||||||
import { backgroundMitt } from '@/modules/emitter';
|
|
||||||
|
|
||||||
const portAudio = require('naudiodon');
|
|
||||||
|
|
||||||
// Audio in and out stream objects.
|
|
||||||
let ai: typeof portAudio.AudioIO | boolean = false;
|
|
||||||
let ao: typeof portAudio.AudioIO | boolean = false;
|
|
||||||
|
|
||||||
// Whether activly recording.
|
|
||||||
let record = false;
|
|
||||||
|
|
||||||
const audioContainer = {
|
|
||||||
input: '',
|
|
||||||
}
|
|
||||||
|
|
||||||
const audioOptions = {
|
|
||||||
channelCount: 1,
|
|
||||||
sampleFormat: 16,
|
|
||||||
sampleRate: 16000,
|
|
||||||
deviceId: -1,
|
|
||||||
closeOnError: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Toggles record to true to begin capturing chunks.
|
|
||||||
const onRecordingStart = (_event: any, _payload: any) => {
|
|
||||||
console.log("AUDIO: Beginning audio capture.")
|
|
||||||
record = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns recorded audio to frontend and sets record to false.
|
|
||||||
const onRecordingEnd = async (_event: any, payload: any) => {
|
|
||||||
console.log("AUDIO:Sending audio to browser.")
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
|
|
||||||
try {
|
|
||||||
resolve(audioContainer.input);
|
|
||||||
record = false;
|
|
||||||
} catch (e) {
|
|
||||||
reject()
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
// Main audio function run by run.ts module.
|
|
||||||
export function initAudioIO(): void {
|
|
||||||
console.log("AUDIO:Starting io streams.")
|
|
||||||
|
|
||||||
if (!ai) {
|
|
||||||
|
|
||||||
// Initialize and start input stream.
|
|
||||||
ai = new portAudio.AudioIO({ inOptions: audioOptions });
|
|
||||||
ai.setEncoding("hex");
|
|
||||||
ai.start();
|
|
||||||
|
|
||||||
// On each data chunk...
|
|
||||||
ai.on('data', (chunk: string) => {
|
|
||||||
|
|
||||||
// If recording, we capture the data.
|
|
||||||
if (record) {
|
|
||||||
console.log('AUDIO:Recording...')
|
|
||||||
audioContainer.input += chunk;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Else, we don't capture and also clear audioContainer.
|
|
||||||
else {
|
|
||||||
if (audioContainer.input.length) {
|
|
||||||
audioContainer.input = "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ao) {
|
|
||||||
|
|
||||||
// Initialize and start input stream.
|
|
||||||
ao = new portAudio.AudioIO({ outOptions: audioOptions });
|
|
||||||
ao.start();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Listen to record.
|
|
||||||
console.log("AUDIO:Adding recording listeners.")
|
|
||||||
|
|
||||||
ipcMain.removeAllListeners("start-recording");
|
|
||||||
ipcMain.on("start-recording", onRecordingStart);
|
|
||||||
|
|
||||||
ipcMain.removeHandler("stop-recording");
|
|
||||||
ipcMain.handle("stop-recording", onRecordingEnd);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// ---Audio playback--------------------------------------------
|
|
||||||
|
|
||||||
// Split Buffer into an array of len-sized Buffers.
|
|
||||||
function bufSplit(buf: Buffer, len: number): Array<Buffer> {
|
|
||||||
const chunks = [];
|
|
||||||
let i = 0;
|
|
||||||
let L = len;
|
|
||||||
|
|
||||||
while(i < buf.byteLength) {
|
|
||||||
chunks.push(buf.slice(i, L));
|
|
||||||
i = L;
|
|
||||||
L += len;
|
|
||||||
}
|
|
||||||
|
|
||||||
return chunks;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Audio playback.
|
|
||||||
export function play(input: string): void {
|
|
||||||
|
|
||||||
// Format the audio.
|
|
||||||
const audio = bufSplit(
|
|
||||||
Buffer.from(input as string, 'hex'),
|
|
||||||
8192
|
|
||||||
);
|
|
||||||
|
|
||||||
// Called on end of write.
|
|
||||||
const callback = () => {
|
|
||||||
|
|
||||||
// We stop audio playback anim.
|
|
||||||
backgroundMitt.emit('ipc-renderer', {
|
|
||||||
endpoint: 'stop-playback-anim'
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
write();
|
|
||||||
|
|
||||||
// Iterate through audio array and write buffers to portAudio writable.
|
|
||||||
function write() {
|
|
||||||
let chunk: Buffer;
|
|
||||||
let ok = true;
|
|
||||||
let i = 0;
|
|
||||||
|
|
||||||
do {
|
|
||||||
chunk = audio[i];
|
|
||||||
if (i === audio.length - 1) {
|
|
||||||
// write last chunk.
|
|
||||||
ao.write(chunk, null, callback);
|
|
||||||
} else {
|
|
||||||
// check for backpreassure.
|
|
||||||
ok = ao.write(chunk, null);
|
|
||||||
}
|
|
||||||
i++;
|
|
||||||
} while (i < audio.length && ok);
|
|
||||||
|
|
||||||
if (i < audio.length) {
|
|
||||||
// Had to stop early!
|
|
||||||
// Write some more once it drains.
|
|
||||||
ao.once('drain', write);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------------------------------------------------
|
|
||||||
|
|
||||||
// Get's called on window close.
|
|
||||||
export async function stopStream() {
|
|
||||||
console.log("AUDIO:Stopping audio stream.")
|
|
||||||
if (ai) {
|
|
||||||
try {
|
|
||||||
await ai.quit()
|
|
||||||
} catch(e){
|
|
||||||
console.log('AUDIO: Failed to shutdown audio input.');
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (ao) {
|
|
||||||
try {
|
|
||||||
await ao.quit()
|
|
||||||
} catch(e){
|
|
||||||
console.log('AUDIO: Failed to shutdown audio output.');
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,62 +0,0 @@
|
||||||
import fs from 'fs';
|
|
||||||
import { backgroundMitt } from "@/modules/emitter";
|
|
||||||
import { SessionState, WindowState } from "@/types";
|
|
||||||
import { app } from "electron";
|
|
||||||
|
|
||||||
const configPath = app.getPath("userData");
|
|
||||||
|
|
||||||
export const ipcEmit = (channel: string, payload: any) => {
|
|
||||||
backgroundMitt.emit('ipc-renderer', {
|
|
||||||
endpoint: channel,
|
|
||||||
message: payload
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export const loadState = (fileName: string): SessionState => {
|
|
||||||
let state: SessionState;
|
|
||||||
|
|
||||||
try {
|
|
||||||
state = JSON.parse(fs.readFileSync(configPath + fileName).toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
catch (error) {
|
|
||||||
state = {
|
|
||||||
key: false,
|
|
||||||
newMessages: []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return state
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export const loadWinState = (fileName: string): WindowState => {
|
|
||||||
let state: WindowState;
|
|
||||||
|
|
||||||
try {
|
|
||||||
state = JSON.parse(fs.readFileSync(configPath + fileName).toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
catch (error) {
|
|
||||||
state = {
|
|
||||||
width: 600,
|
|
||||||
height: 500,
|
|
||||||
x: null,
|
|
||||||
y: null,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return state
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save session or window state.
|
|
||||||
export const saveToJson = (fileName: string, data: any) => {
|
|
||||||
|
|
||||||
fs.writeFile(configPath + fileName, JSON.stringify(data), (err) => {
|
|
||||||
if (err) {
|
|
||||||
console.log("Error when saving to json.")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,90 +0,0 @@
|
||||||
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
import { app, dialog } from "electron";
|
|
||||||
import { createWindow } from './window';
|
|
||||||
import { initSession } from './session';
|
|
||||||
import { initAudioIO, stopStream } from './audio';
|
|
||||||
import { backgroundMitt } from '@/modules/emitter';
|
|
||||||
|
|
||||||
let win: boolean;
|
|
||||||
|
|
||||||
// Listen for window creation.
|
|
||||||
backgroundMitt.on('window-active', (state: boolean) => {
|
|
||||||
win = state;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Auto updating.
|
|
||||||
const { autoUpdater } = require('electron-updater')
|
|
||||||
autoUpdater.requestHeaders = { 'PRIVATE-TOKEN': 'mvvgWYwWnot4bisiQMh_' }
|
|
||||||
|
|
||||||
autoUpdater.on('update-available', (info: any) => {
|
|
||||||
console.log(`Update available: ${info.version}`)
|
|
||||||
})
|
|
||||||
|
|
||||||
autoUpdater.on('update-downloaded', (info: any) => {
|
|
||||||
|
|
||||||
const updateDialog = {
|
|
||||||
type: 'info',
|
|
||||||
buttons: ['Restart', 'Later'],
|
|
||||||
title: 'Application Update',
|
|
||||||
message: info.version,
|
|
||||||
detail: 'A new version has been downloaded. Restart the application to apply the updates.'
|
|
||||||
}
|
|
||||||
|
|
||||||
dialog.showMessageBox(updateDialog).then((returnValue) => {
|
|
||||||
if (returnValue.response === 0) autoUpdater.quitAndInstall()
|
|
||||||
})
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
// Run when electron app is initialized.
|
|
||||||
async function main() {
|
|
||||||
console.log("MAIN:Initializing Electron App.")
|
|
||||||
|
|
||||||
// Must wait til window is created.
|
|
||||||
await createWindow();
|
|
||||||
|
|
||||||
// Instantiate socket session with crimata-platorm.
|
|
||||||
initSession();
|
|
||||||
|
|
||||||
// Begin audio stream.
|
|
||||||
initAudioIO();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Root function of app.
|
|
||||||
export function initApp(dev: boolean): void {
|
|
||||||
|
|
||||||
// On initial startup.
|
|
||||||
app.on("ready", () => {
|
|
||||||
autoUpdater.checkForUpdates()
|
|
||||||
main()
|
|
||||||
});
|
|
||||||
|
|
||||||
// Must keep to ensure app doesn't quit on close.
|
|
||||||
app.on("before-quit", async () => {
|
|
||||||
await stopStream();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Must keep to ensure app doesn't quit on close.
|
|
||||||
app.on("window-all-closed", () => {
|
|
||||||
});
|
|
||||||
|
|
||||||
// When user clicks app icon (re-open)
|
|
||||||
app.on("activate", () => {
|
|
||||||
|
|
||||||
if (!win) {
|
|
||||||
createWindow();
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
// Exit cleanly on request from parent process in development mode.
|
|
||||||
if (dev) {
|
|
||||||
process.on("SIGTERM", () => {
|
|
||||||
app.quit();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,166 +0,0 @@
|
||||||
/*
|
|
||||||
* Creates a websocket session with Crimata Servers.
|
|
||||||
*
|
|
||||||
* Connects to Servers and attempts key authentication. Server will respond
|
|
||||||
* with key and user profile. We send the profile to the browser. We also
|
|
||||||
* resend this information on new broser window. We then serve as a
|
|
||||||
* communication interface between the window and the servers. It will
|
|
||||||
* automatically try to reconnect on websocket disconnect.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { backgroundMitt } from "@/modules/emitter";
|
|
||||||
import { ipcEmit, loadState, saveToJson } from './helpers';
|
|
||||||
import { ipcMain, IpcMainEvent, IpcMainInvokeEvent } from "electron";
|
|
||||||
|
|
||||||
import useWebSockets from "@/modules/websockets";
|
|
||||||
|
|
||||||
import { play } from "./audio";
|
|
||||||
import { renderMessage } from "@/modules/message";
|
|
||||||
import { AuthProtocol, SessionState, Profile } from "@/types";
|
|
||||||
|
|
||||||
let win = true;
|
|
||||||
|
|
||||||
// Info saved to json on quit (key, newMessages).
|
|
||||||
let state: SessionState;
|
|
||||||
|
|
||||||
// Profile of current user.
|
|
||||||
let profile: Profile | boolean;
|
|
||||||
|
|
||||||
// Called when server sends auth message.
|
|
||||||
const updateState = (res: AuthProtocol) => {
|
|
||||||
console.log("SESS:Auth message received: \n" +
|
|
||||||
` key: ${res.key}\n` +
|
|
||||||
` alias: ${res.profile}`)
|
|
||||||
|
|
||||||
if (state) {
|
|
||||||
|
|
||||||
// Update key.
|
|
||||||
state.key = res.key;
|
|
||||||
|
|
||||||
// Update the user profile.
|
|
||||||
profile = res.profile;
|
|
||||||
|
|
||||||
// Send upated profile to frontend.
|
|
||||||
console.log("SESS:Sending updated user profile to browser.")
|
|
||||||
ipcEmit("update-state", {
|
|
||||||
profile: profile,
|
|
||||||
newMessages: state.newMessages
|
|
||||||
})
|
|
||||||
|
|
||||||
// Save the updated state to json.
|
|
||||||
console.log("SESS:Saving session state.")
|
|
||||||
saveToJson("session.json", state)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send state on new window.
|
|
||||||
const onNewBrowserWindow = (_event: IpcMainInvokeEvent, _payload: any) => {
|
|
||||||
if (typeof profile !== 'undefined') {
|
|
||||||
console.log("SESS:Sending user profile to browser.")
|
|
||||||
ipcEmit("update-state", {
|
|
||||||
profile: profile,
|
|
||||||
newMessages: state.newMessages
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calls appropriate endpoint for a server message.
|
|
||||||
const onMessage = (data: string) => {
|
|
||||||
let message = JSON.parse(data)
|
|
||||||
|
|
||||||
// AuthProtocol message.
|
|
||||||
if (message.hasOwnProperty("key")) {
|
|
||||||
updateState(message)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Standard message.
|
|
||||||
else if (message.content) {
|
|
||||||
|
|
||||||
// Convert to render message
|
|
||||||
message = renderMessage(
|
|
||||||
message.content.text,
|
|
||||||
message.content.audio,
|
|
||||||
message.context,
|
|
||||||
message.modifier
|
|
||||||
);
|
|
||||||
|
|
||||||
if (win) {
|
|
||||||
console.log("SESS:Emitting standard message.")
|
|
||||||
if (message.audio) {
|
|
||||||
play(message.audio)
|
|
||||||
}
|
|
||||||
ipcEmit("render-message", message)
|
|
||||||
}
|
|
||||||
|
|
||||||
else {
|
|
||||||
console.log("SESS:No window: saving message.")
|
|
||||||
state.newMessages.push(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else {
|
|
||||||
if (win) {
|
|
||||||
ipcEmit("render-message", message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
// When socket connects, we update state.
|
|
||||||
const onOpen = () => {
|
|
||||||
console.log(`SESS:Sending key: ${state.key}`)
|
|
||||||
|
|
||||||
if (state) {
|
|
||||||
sendMessage({
|
|
||||||
"key": state.key,
|
|
||||||
"usr": false,
|
|
||||||
"pwd": false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Websockets module.
|
|
||||||
const { createSocket, sendMessage } = useWebSockets(onMessage, onOpen);
|
|
||||||
|
|
||||||
// Handle messages from window/client.
|
|
||||||
const onClientMessage = (_event: IpcMainEvent, payload: any) => {
|
|
||||||
console.log("New client message")
|
|
||||||
|
|
||||||
const success = sendMessage(payload)
|
|
||||||
|
|
||||||
if (!success) {
|
|
||||||
console.log("Unable to send message: ", payload)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call this to initialize session with Crimata servers.
|
|
||||||
export const initSession = () => {
|
|
||||||
console.log("SESS:Creating new session.")
|
|
||||||
|
|
||||||
// Load Json or createState.
|
|
||||||
state = loadState("session.json")
|
|
||||||
|
|
||||||
console.log("SESS:State loaded: \n" +
|
|
||||||
` key: ${state.key}\n` +
|
|
||||||
` new: ${state.newMessages}`)
|
|
||||||
|
|
||||||
// Open socket connection.
|
|
||||||
createSocket()
|
|
||||||
|
|
||||||
// Attack browser window init listener.
|
|
||||||
ipcMain.removeAllListeners("app-mounted")
|
|
||||||
ipcMain.on("app-mounted", onNewBrowserWindow);
|
|
||||||
|
|
||||||
// Attach listeners for frontend.
|
|
||||||
ipcMain.removeAllListeners("client-message")
|
|
||||||
ipcMain.on("client-message", onClientMessage);
|
|
||||||
|
|
||||||
// Keep win up-to-date.
|
|
||||||
backgroundMitt.on('window-active', (state: boolean) => {
|
|
||||||
win = state;
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,111 +0,0 @@
|
||||||
import anime from "animejs";
|
|
||||||
import useMitt from "@/modules/mitt";
|
|
||||||
import { useIpc } from '@/modules/ipc';
|
|
||||||
import { onMounted, onUnmounted, ref, Ref } from "vue";
|
|
||||||
import keyboardNameMap from "../keyBoardMaps/keyboardNameMap";
|
|
||||||
import { renderMessage, clientMessage } from '@/modules/message';
|
|
||||||
|
|
||||||
|
|
||||||
function showRecIcon () {
|
|
||||||
|
|
||||||
anime({
|
|
||||||
targets: '#recIcon',
|
|
||||||
opacity: [0, 0.75],
|
|
||||||
scale: [0.0, 1],
|
|
||||||
duration: 250,
|
|
||||||
easing: 'linear',
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function hideRecIcon () {
|
|
||||||
|
|
||||||
anime({
|
|
||||||
targets: '#recIcon',
|
|
||||||
opacity: [0.75, 0],
|
|
||||||
scale: [1, 0],
|
|
||||||
duration: 250,
|
|
||||||
easing: 'linear',
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export default function useAudioInputController (typing: Ref) {
|
|
||||||
|
|
||||||
// For sending messages.
|
|
||||||
const { post, invoke } = useIpc();
|
|
||||||
const { emitter } = useMitt();
|
|
||||||
|
|
||||||
// Keepp track of when we are recording.
|
|
||||||
const recording = ref(false);
|
|
||||||
|
|
||||||
//---Callbacks-----------------------------------------------
|
|
||||||
|
|
||||||
const onKeyDown = (e: KeyboardEvent) => {
|
|
||||||
const cmd = keyboardNameMap[e.keyCode];
|
|
||||||
// console.log(cmd)
|
|
||||||
|
|
||||||
// Start recording on space bar.
|
|
||||||
if (cmd == "SPACE" && !recording.value && !typing.value) {
|
|
||||||
|
|
||||||
console.log("INPT:Starting record.")
|
|
||||||
post("start-recording", "");
|
|
||||||
|
|
||||||
showRecIcon()
|
|
||||||
recording.value = true;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
const onKeyUp = async (e: KeyboardEvent) => {
|
|
||||||
const cmd = keyboardNameMap[e.keyCode];
|
|
||||||
|
|
||||||
// Stop recording on space up.
|
|
||||||
if (cmd == "SPACE" && recording.value) {
|
|
||||||
|
|
||||||
// Create a message.
|
|
||||||
const message = renderMessage(
|
|
||||||
"",
|
|
||||||
"",
|
|
||||||
"",
|
|
||||||
"sf"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Render it immediately.
|
|
||||||
emitter.emit("self-message", message);
|
|
||||||
|
|
||||||
// Stop recording and get audio from recorder.
|
|
||||||
console.log("INPT:Stopping record.")
|
|
||||||
const audio = await invoke("stop-recording", "");
|
|
||||||
|
|
||||||
// Send message to the backend for processing.
|
|
||||||
const clientM = clientMessage("", audio, message.uid);
|
|
||||||
post('client-message', clientM);
|
|
||||||
|
|
||||||
hideRecIcon()
|
|
||||||
recording.value = false;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
//-----------------------------------------------------------
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
window.addEventListener("keydown", onKeyDown);
|
|
||||||
window.addEventListener("keyup", onKeyUp);
|
|
||||||
})
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
window.removeEventListener("keydown", onKeyDown);
|
|
||||||
window.removeEventListener("keyup", onKeyUp);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
return {
|
|
||||||
recording
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,263 +0,0 @@
|
||||||
// This has the UnShifted and Shifted characters that each key maps to
|
|
||||||
// Ones that are to be ignored for character input are empty.
|
|
||||||
const keyboardCharMap = [
|
|
||||||
["", ""], // [0]
|
|
||||||
["", ""], // [1]
|
|
||||||
["", ""], // [2]
|
|
||||||
["", ""], // [3]
|
|
||||||
["", ""], // [4]
|
|
||||||
["", ""], // [5]
|
|
||||||
["", ""], // [6]
|
|
||||||
["", ""], // [7]
|
|
||||||
["", ""], // [8]
|
|
||||||
["", ""], // [9]
|
|
||||||
["", ""], // [10]
|
|
||||||
["", ""], // [11]
|
|
||||||
["", ""], // [12]
|
|
||||||
["\r", "\r"], // [13] - MOST control characters are ignored. This one (Carriage Return, or "Enter") is significant!
|
|
||||||
["", ""], // [14]
|
|
||||||
["", ""], // [15]
|
|
||||||
["", ""], // [16]
|
|
||||||
["", ""], // [17]
|
|
||||||
["", ""], // [18]
|
|
||||||
["", ""], // [19]
|
|
||||||
["", ""], // [20]
|
|
||||||
["", ""], // [21]
|
|
||||||
["", ""], // [22]
|
|
||||||
["", ""], // [23]
|
|
||||||
["", ""], // [24]
|
|
||||||
["", ""], // [25]
|
|
||||||
["", ""], // [26]
|
|
||||||
["", ""], // [27]
|
|
||||||
["", ""], // [28]
|
|
||||||
["", ""], // [29]
|
|
||||||
["", ""], // [30]
|
|
||||||
["", ""], // [31]
|
|
||||||
[" ", " "], // [32] // SPACE! Don't "clean it up" and remove the space!
|
|
||||||
["", ""], // [33]
|
|
||||||
["", ""], // [34]
|
|
||||||
["", ""], // [35]
|
|
||||||
["", ""], // [36]
|
|
||||||
["", ""], // [37]
|
|
||||||
["", ""], // [38]
|
|
||||||
["", ""], // [39]
|
|
||||||
["", ""], // [40]
|
|
||||||
["", ""], // [41]
|
|
||||||
["", ""], // [42]
|
|
||||||
["", ""], // [43]
|
|
||||||
["", ""], // [44]
|
|
||||||
["", ""], // [45]
|
|
||||||
["", ""], // [46]
|
|
||||||
["", ""], // [47]
|
|
||||||
["0", ")"], // [48]
|
|
||||||
["1", "!"], // [49]
|
|
||||||
["2", "@"], // [50]
|
|
||||||
["3", "#"], // [51]
|
|
||||||
["4", "$"], // [52]
|
|
||||||
["5", "%"], // [53]
|
|
||||||
["6", "^"], // [54]
|
|
||||||
["7", "&"], // [55]
|
|
||||||
["8", "*"], // [56]
|
|
||||||
["9", "("], // [57]
|
|
||||||
["", ""], // [58]
|
|
||||||
[";", ":"], // [59]
|
|
||||||
["<", ""], // [60]
|
|
||||||
["=", ""], // [61]
|
|
||||||
[">", ""], // [62]
|
|
||||||
["?", ""], // [63] shifted; else "/"
|
|
||||||
["", ""], // [64]
|
|
||||||
["a", "A"], // [65]
|
|
||||||
["b", "B"], // [66]
|
|
||||||
["c", "C"], // [67]
|
|
||||||
["d", "D"], // [68]
|
|
||||||
["e", "E"], // [69]
|
|
||||||
["f", "F"], // [70]
|
|
||||||
["g", "G"], // [71]
|
|
||||||
["h", "H"], // [72]
|
|
||||||
["i", "I"], // [73]
|
|
||||||
["j", "J"], // [74]
|
|
||||||
["k", "K"], // [75]
|
|
||||||
["l", "L"], // [76]
|
|
||||||
["m", "M"], // [77]
|
|
||||||
["n", "N"], // [78]
|
|
||||||
["o", "O"], // [79]
|
|
||||||
["p", "P"], // [80]
|
|
||||||
["q", "Q"], // [81]
|
|
||||||
["r", "R"], // [82]
|
|
||||||
["s", "S"], // [83]
|
|
||||||
["t", "T"], // [84]
|
|
||||||
["u", "U"], // [85]
|
|
||||||
["v", "V"], // [86]
|
|
||||||
["w", "W"], // [87]
|
|
||||||
["x", "X"], // [88]
|
|
||||||
["y", "Y"], // [89]
|
|
||||||
["z", "Z"], // [90]
|
|
||||||
["", ""], // [91] Windows Key (Windows) or Command Key (Mac)
|
|
||||||
["", ""], // [92]
|
|
||||||
["", ""], // [93]
|
|
||||||
["", ""], // [94]
|
|
||||||
["", ""], // [95]
|
|
||||||
// Number Keypad Entries...
|
|
||||||
["0", ""], // [96]
|
|
||||||
["1", ""], // [97]
|
|
||||||
["2", ""], // [98]
|
|
||||||
["3", ""], // [99]
|
|
||||||
["4", ""], // [100]
|
|
||||||
["5", ""], // [101]
|
|
||||||
["6", ""], // [102]
|
|
||||||
["7", ""], // [103]
|
|
||||||
["8", ""], // [104]
|
|
||||||
["9", ""], // [105]
|
|
||||||
["*", ""], // [106]
|
|
||||||
["+", ""], // [107]
|
|
||||||
["", ""], // [108]
|
|
||||||
["-", ""], // [109]
|
|
||||||
[".", ""], // [110]
|
|
||||||
["/", ""], // [111]
|
|
||||||
|
|
||||||
["", ""], // [112]
|
|
||||||
["", ""], // [113]
|
|
||||||
["", ""], // [114]
|
|
||||||
["", ""], // [115]
|
|
||||||
["", ""], // [116]
|
|
||||||
["", ""], // [117]
|
|
||||||
["", ""], // [118]
|
|
||||||
["", ""], // [119]
|
|
||||||
["", ""], // [120]
|
|
||||||
["", ""], // [121]
|
|
||||||
["", ""], // [122]
|
|
||||||
["", ""], // [123]
|
|
||||||
["", ""], // [124]
|
|
||||||
["", ""], // [125]
|
|
||||||
["", ""], // [126]
|
|
||||||
["", ""], // [127]
|
|
||||||
["", ""], // [128]
|
|
||||||
["", ""], // [129]
|
|
||||||
["", ""], // [130]
|
|
||||||
["", ""], // [131]
|
|
||||||
["", ""], // [132]
|
|
||||||
["", ""], // [133]
|
|
||||||
["", ""], // [134]
|
|
||||||
["", ""], // [135]
|
|
||||||
["", ""], // [136]
|
|
||||||
["", ""], // [137]
|
|
||||||
["", ""], // [138]
|
|
||||||
["", ""], // [139]
|
|
||||||
["", ""], // [140]
|
|
||||||
["", ""], // [141]
|
|
||||||
["", ""], // [142]
|
|
||||||
["", ""], // [143]
|
|
||||||
["", ""], // [144]
|
|
||||||
["", ""], // [145]
|
|
||||||
["", ""], // [146]
|
|
||||||
["", ""], // [147]
|
|
||||||
["", ""], // [148]
|
|
||||||
["", ""], // [149]
|
|
||||||
["", ""], // [150]
|
|
||||||
["", ""], // [151]
|
|
||||||
["", ""], // [152]
|
|
||||||
["", ""], // [153]
|
|
||||||
["", ""], // [154]
|
|
||||||
["", ""], // [155]
|
|
||||||
["", ""], // [156]
|
|
||||||
["", ""], // [157]
|
|
||||||
["", ""], // [158]
|
|
||||||
["", ""], // [159]
|
|
||||||
["", ""], // [160]
|
|
||||||
["", ""], // [161]
|
|
||||||
["", ""], // [162]
|
|
||||||
["", ""], // [163]
|
|
||||||
["", ""], // [164]
|
|
||||||
["", ""], // [165]
|
|
||||||
["", ""], // [166]
|
|
||||||
["", ""], // [167]
|
|
||||||
["", ""], // [168]
|
|
||||||
["", ""], // [169]
|
|
||||||
["", ""], // [170]
|
|
||||||
["", ""], // [171]
|
|
||||||
["", ""], // [172]
|
|
||||||
["", ""], // [173]
|
|
||||||
["", ""], // [174]
|
|
||||||
["", ""], // [175]
|
|
||||||
["", ""], // [176]
|
|
||||||
["", ""], // [177]
|
|
||||||
["", ""], // [178]
|
|
||||||
["", ""], // [179]
|
|
||||||
["", ""], // [180]
|
|
||||||
["", ""], // [181]
|
|
||||||
["", ""], // [182]
|
|
||||||
["", ""], // [183]
|
|
||||||
["", ""], // [184]
|
|
||||||
["", ""], // [185]
|
|
||||||
[";", ":"], // [186]
|
|
||||||
["=", "+"], // [187]
|
|
||||||
[",", "<"], // [188]
|
|
||||||
["-", "_"], // [189]
|
|
||||||
[".", ">"], // [190]
|
|
||||||
["/", "?"], // [191]
|
|
||||||
["`", "~"], // [192]
|
|
||||||
["", ""], // [193]
|
|
||||||
["", ""], // [194]
|
|
||||||
["", ""], // [195]
|
|
||||||
["", ""], // [196]
|
|
||||||
["", ""], // [197]
|
|
||||||
["", ""], // [198]
|
|
||||||
["", ""], // [199]
|
|
||||||
["", ""], // [200]
|
|
||||||
["", ""], // [201]
|
|
||||||
["", ""], // [202]
|
|
||||||
["", ""], // [203]
|
|
||||||
["", ""], // [204]
|
|
||||||
["", ""], // [205]
|
|
||||||
["", ""], // [206]
|
|
||||||
["", ""], // [207]
|
|
||||||
["", ""], // [208]
|
|
||||||
["", ""], // [209]
|
|
||||||
["", ""], // [210]
|
|
||||||
["", ""], // [211]
|
|
||||||
["", ""], // [212]
|
|
||||||
["", ""], // [213]
|
|
||||||
["", ""], // [214]
|
|
||||||
["", ""], // [215]
|
|
||||||
["", ""], // [216]
|
|
||||||
["", ""], // [217]
|
|
||||||
["", ""], // [218]
|
|
||||||
["[", "{"], // [219]
|
|
||||||
["\\", "|"], // [220]
|
|
||||||
["]", "}"], // [221]
|
|
||||||
["'", '"'], // [222]
|
|
||||||
["", ""], // [223]
|
|
||||||
["", ""], // [224]
|
|
||||||
["", ""], // [225]
|
|
||||||
["", ""], // [226]
|
|
||||||
["", ""], // [227]
|
|
||||||
["", ""], // [228]
|
|
||||||
["", ""], // [229]
|
|
||||||
["", ""], // [230]
|
|
||||||
["", ""], // [231]
|
|
||||||
["", ""], // [232]
|
|
||||||
["", ""], // [233]
|
|
||||||
["", ""], // [234]
|
|
||||||
["", ""], // [235]
|
|
||||||
["", ""], // [236]
|
|
||||||
["", ""], // [237]
|
|
||||||
["", ""], // [238]
|
|
||||||
["", ""], // [239]
|
|
||||||
["", ""], // [240]
|
|
||||||
["", ""], // [241]
|
|
||||||
["", ""], // [242]
|
|
||||||
["", ""], // [243]
|
|
||||||
["", ""], // [244]
|
|
||||||
["", ""], // [245]
|
|
||||||
["", ""], // [246]
|
|
||||||
["", ""], // [247]
|
|
||||||
["", ""], // [248]
|
|
||||||
["", ""], // [249]
|
|
||||||
["", ""], // [250]
|
|
||||||
["", ""], // [251]
|
|
||||||
["", ""], // [252]
|
|
||||||
["", ""], // [253]
|
|
||||||
["", ""], // [254]
|
|
||||||
["", ""] // [255]
|
|
||||||
];
|
|
||||||
export default keyboardCharMap;
|
|
||||||
|
|
@ -1,261 +0,0 @@
|
||||||
// names of known key codes (0-255)
|
|
||||||
const keyboardNameMap = [
|
|
||||||
"", // [0]
|
|
||||||
"", // [1]
|
|
||||||
"", // [2]
|
|
||||||
"CANCEL", // [3]
|
|
||||||
"", // [4]
|
|
||||||
"", // [5]
|
|
||||||
"HELP", // [6]
|
|
||||||
"", // [7]
|
|
||||||
"BACK_SPACE", // [8]
|
|
||||||
"TAB", // [9]
|
|
||||||
"", // [10]
|
|
||||||
"", // [11]
|
|
||||||
"CLEAR", // [12]
|
|
||||||
"ENTER", // [13]
|
|
||||||
"ENTER_SPECIAL", // [14]
|
|
||||||
"", // [15]
|
|
||||||
"SHIFT", // [16]
|
|
||||||
"CONTROL", // [17]
|
|
||||||
"ALT", // [18]
|
|
||||||
"PAUSE", // [19]
|
|
||||||
"CAPS_LOCK", // [20]
|
|
||||||
"KANA", // [21]
|
|
||||||
"EISU", // [22]
|
|
||||||
"JUNJA", // [23]
|
|
||||||
"FINAL", // [24]
|
|
||||||
"HANJA", // [25]
|
|
||||||
"", // [26]
|
|
||||||
"ESCAPE", // [27]
|
|
||||||
"CONVERT", // [28]
|
|
||||||
"NONCONVERT", // [29]
|
|
||||||
"ACCEPT", // [30]
|
|
||||||
"MODECHANGE", // [31]
|
|
||||||
"SPACE", // [32]
|
|
||||||
"PAGE_UP", // [33]
|
|
||||||
"PAGE_DOWN", // [34]
|
|
||||||
"END", // [35]
|
|
||||||
"HOME", // [36]
|
|
||||||
"LEFT", // [37]
|
|
||||||
"UP", // [38]
|
|
||||||
"RIGHT", // [39]
|
|
||||||
"DOWN", // [40]
|
|
||||||
"SELECT", // [41]
|
|
||||||
"PRINT", // [42]
|
|
||||||
"EXECUTE", // [43]
|
|
||||||
"PRINTSCREEN", // [44]
|
|
||||||
"INSERT", // [45]
|
|
||||||
"DELETE", // [46]
|
|
||||||
"", // [47]
|
|
||||||
"0", // [48]
|
|
||||||
"1", // [49]
|
|
||||||
"2", // [50]
|
|
||||||
"3", // [51]
|
|
||||||
"4", // [52]
|
|
||||||
"5", // [53]
|
|
||||||
"6", // [54]
|
|
||||||
"7", // [55]
|
|
||||||
"8", // [56]
|
|
||||||
"9", // [57]
|
|
||||||
"COLON", // [58]
|
|
||||||
"SEMICOLON", // [59]
|
|
||||||
"LESS_THAN", // [60]
|
|
||||||
"EQUALS", // [61]
|
|
||||||
"GREATER_THAN", // [62]
|
|
||||||
"QUESTION_MARK", // [63]
|
|
||||||
"AT", // [64]
|
|
||||||
"A", // [65]
|
|
||||||
"B", // [66]
|
|
||||||
"C", // [67]
|
|
||||||
"D", // [68]
|
|
||||||
"E", // [69]
|
|
||||||
"F", // [70]
|
|
||||||
"G", // [71]
|
|
||||||
"H", // [72]
|
|
||||||
"I", // [73]
|
|
||||||
"J", // [74]
|
|
||||||
"K", // [75]
|
|
||||||
"L", // [76]
|
|
||||||
"M", // [77]
|
|
||||||
"N", // [78]
|
|
||||||
"O", // [79]
|
|
||||||
"P", // [80]
|
|
||||||
"Q", // [81]
|
|
||||||
"R", // [82]
|
|
||||||
"S", // [83]
|
|
||||||
"T", // [84]
|
|
||||||
"U", // [85]
|
|
||||||
"V", // [86]
|
|
||||||
"W", // [87]
|
|
||||||
"X", // [88]
|
|
||||||
"Y", // [89]
|
|
||||||
"Z", // [90]
|
|
||||||
"OS_KEY", // [91] Windows Key (Windows) or Command Key (Mac)
|
|
||||||
"", // [92]
|
|
||||||
"CONTEXT_MENU", // [93]
|
|
||||||
"", // [94]
|
|
||||||
"SLEEP", // [95]
|
|
||||||
"NUMPAD0", // [96]
|
|
||||||
"NUMPAD1", // [97]
|
|
||||||
"NUMPAD2", // [98]
|
|
||||||
"NUMPAD3", // [99]
|
|
||||||
"NUMPAD4", // [100]
|
|
||||||
"NUMPAD5", // [101]
|
|
||||||
"NUMPAD6", // [102]
|
|
||||||
"NUMPAD7", // [103]
|
|
||||||
"NUMPAD8", // [104]
|
|
||||||
"NUMPAD9", // [105]
|
|
||||||
"MULTIPLY", // [106]
|
|
||||||
"ADD", // [107]
|
|
||||||
"SEPARATOR", // [108]
|
|
||||||
"SUBTRACT", // [109]
|
|
||||||
"DECIMAL", // [110]
|
|
||||||
"DIVIDE", // [111]
|
|
||||||
"F1", // [112]
|
|
||||||
"F2", // [113]
|
|
||||||
"F3", // [114]
|
|
||||||
"F4", // [115]
|
|
||||||
"F5", // [116]
|
|
||||||
"F6", // [117]
|
|
||||||
"F7", // [118]
|
|
||||||
"F8", // [119]
|
|
||||||
"F9", // [120]
|
|
||||||
"F10", // [121]
|
|
||||||
"F11", // [122]
|
|
||||||
"F12", // [123]
|
|
||||||
"F13", // [124]
|
|
||||||
"F14", // [125]
|
|
||||||
"F15", // [126]
|
|
||||||
"F16", // [127]
|
|
||||||
"F17", // [128]
|
|
||||||
"F18", // [129]
|
|
||||||
"F19", // [130]
|
|
||||||
"F20", // [131]
|
|
||||||
"F21", // [132]
|
|
||||||
"F22", // [133]
|
|
||||||
"F23", // [134]
|
|
||||||
"F24", // [135]
|
|
||||||
"", // [136]
|
|
||||||
"", // [137]
|
|
||||||
"", // [138]
|
|
||||||
"", // [139]
|
|
||||||
"", // [140]
|
|
||||||
"", // [141]
|
|
||||||
"", // [142]
|
|
||||||
"", // [143]
|
|
||||||
"NUM_LOCK", // [144]
|
|
||||||
"SCROLL_LOCK", // [145]
|
|
||||||
"WIN_OEM_FJ_JISHO", // [146]
|
|
||||||
"WIN_OEM_FJ_MASSHOU", // [147]
|
|
||||||
"WIN_OEM_FJ_TOUROKU", // [148]
|
|
||||||
"WIN_OEM_FJ_LOYA", // [149]
|
|
||||||
"WIN_OEM_FJ_ROYA", // [150]
|
|
||||||
"", // [151]
|
|
||||||
"", // [152]
|
|
||||||
"", // [153]
|
|
||||||
"", // [154]
|
|
||||||
"", // [155]
|
|
||||||
"", // [156]
|
|
||||||
"", // [157]
|
|
||||||
"", // [158]
|
|
||||||
"", // [159]
|
|
||||||
"CIRCUMFLEX", // [160]
|
|
||||||
"EXCLAMATION", // [161]
|
|
||||||
"DOUBLE_QUOTE", // [162]
|
|
||||||
"HASH", // [163]
|
|
||||||
"DOLLAR", // [164]
|
|
||||||
"PERCENT", // [165]
|
|
||||||
"AMPERSAND", // [166]
|
|
||||||
"UNDERSCORE", // [167]
|
|
||||||
"OPEN_PAREN", // [168]
|
|
||||||
"CLOSE_PAREN", // [169]
|
|
||||||
"ASTERISK", // [170]
|
|
||||||
"PLUS", // [171]
|
|
||||||
"PIPE", // [172]
|
|
||||||
"HYPHEN_MINUS", // [173]
|
|
||||||
"OPEN_CURLY_BRACKET", // [174]
|
|
||||||
"CLOSE_CURLY_BRACKET", // [175]
|
|
||||||
"TILDE", // [176]
|
|
||||||
"", // [177]
|
|
||||||
"", // [178]
|
|
||||||
"", // [179]
|
|
||||||
"", // [180]
|
|
||||||
"VOLUME_MUTE", // [181]
|
|
||||||
"VOLUME_DOWN", // [182]
|
|
||||||
"VOLUME_UP", // [183]
|
|
||||||
"", // [184]
|
|
||||||
"", // [185]
|
|
||||||
"SEMICOLON", // [186]
|
|
||||||
"EQUALS", // [187]
|
|
||||||
"COMMA", // [188]
|
|
||||||
"MINUS", // [189]
|
|
||||||
"PERIOD", // [190]
|
|
||||||
"SLASH", // [191]
|
|
||||||
"BACK_QUOTE", // [192]
|
|
||||||
"", // [193]
|
|
||||||
"", // [194]
|
|
||||||
"", // [195]
|
|
||||||
"", // [196]
|
|
||||||
"", // [197]
|
|
||||||
"", // [198]
|
|
||||||
"", // [199]
|
|
||||||
"", // [200]
|
|
||||||
"", // [201]
|
|
||||||
"", // [202]
|
|
||||||
"", // [203]
|
|
||||||
"", // [204]
|
|
||||||
"", // [205]
|
|
||||||
"", // [206]
|
|
||||||
"", // [207]
|
|
||||||
"", // [208]
|
|
||||||
"", // [209]
|
|
||||||
"", // [210]
|
|
||||||
"", // [211]
|
|
||||||
"", // [212]
|
|
||||||
"", // [213]
|
|
||||||
"", // [214]
|
|
||||||
"", // [215]
|
|
||||||
"", // [216]
|
|
||||||
"", // [217]
|
|
||||||
"", // [218]
|
|
||||||
"OPEN_BRACKET", // [219]
|
|
||||||
"BACK_SLASH", // [220]
|
|
||||||
"CLOSE_BRACKET", // [221]
|
|
||||||
"QUOTE", // [222]
|
|
||||||
"", // [223]
|
|
||||||
"META", // [224]
|
|
||||||
"ALTGR", // [225]
|
|
||||||
"", // [226]
|
|
||||||
"WIN_ICO_HELP", // [227]
|
|
||||||
"WIN_ICO_00", // [228]
|
|
||||||
"", // [229]
|
|
||||||
"WIN_ICO_CLEAR", // [230]
|
|
||||||
"", // [231]
|
|
||||||
"", // [232]
|
|
||||||
"WIN_OEM_RESET", // [233]
|
|
||||||
"WIN_OEM_JUMP", // [234]
|
|
||||||
"WIN_OEM_PA1", // [235]
|
|
||||||
"WIN_OEM_PA2", // [236]
|
|
||||||
"WIN_OEM_PA3", // [237]
|
|
||||||
"WIN_OEM_WSCTRL", // [238]
|
|
||||||
"WIN_OEM_CUSEL", // [239]
|
|
||||||
"WIN_OEM_ATTN", // [240]
|
|
||||||
"WIN_OEM_FINISH", // [241]
|
|
||||||
"WIN_OEM_COPY", // [242]
|
|
||||||
"WIN_OEM_AUTO", // [243]
|
|
||||||
"WIN_OEM_ENLW", // [244]
|
|
||||||
"WIN_OEM_BACKTAB", // [245]
|
|
||||||
"ATTN", // [246]
|
|
||||||
"CRSEL", // [247]
|
|
||||||
"EXSEL", // [248]
|
|
||||||
"EREOF", // [249]
|
|
||||||
"PLAY", // [250]
|
|
||||||
"ZOOM", // [251]
|
|
||||||
"", // [252]
|
|
||||||
"PA1", // [253]
|
|
||||||
"WIN_OEM_CLEAR", // [254]
|
|
||||||
"" // [255]
|
|
||||||
];
|
|
||||||
|
|
||||||
export default keyboardNameMap;
|
|
||||||
|
|
@ -1,419 +0,0 @@
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
class="message"
|
|
||||||
:class="`${message.isChild}ChildMessage`"
|
|
||||||
:id="`${message.modifier}Message`"
|
|
||||||
:key="message.id"
|
|
||||||
>
|
|
||||||
|
|
||||||
<!-- Three types of messages: self (sf), friend (fr), and crimata (ai) -->
|
|
||||||
<!-- !denoted by the modifier attribute -->
|
|
||||||
|
|
||||||
<!-- Shows when a new session is created with Crimata. -->
|
|
||||||
<div
|
|
||||||
v-if="message.context === 'Crimata'"
|
|
||||||
class="divider"
|
|
||||||
>
|
|
||||||
<div>new session</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Includes bubble and context. -->
|
|
||||||
<div
|
|
||||||
:id="`${message.modifier}MessageBox`"
|
|
||||||
class="messageBox"
|
|
||||||
>
|
|
||||||
|
|
||||||
<!-- message bubble -->
|
|
||||||
<div
|
|
||||||
class="bubble"
|
|
||||||
:class="`${message.modifier}-${message.isChild}ChildBubble`"
|
|
||||||
:id="`${message.modifier}Bubble`"
|
|
||||||
>
|
|
||||||
|
|
||||||
<!-- Notification dot for new messages. -->
|
|
||||||
<div v-if="notify === true"
|
|
||||||
class="notify"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Show loader when audio is being transcribed. -->
|
|
||||||
<div
|
|
||||||
v-if="message.content.text === ''"
|
|
||||||
class="contentLoader"
|
|
||||||
>
|
|
||||||
<div class="contentLoaderDot"></div>
|
|
||||||
<div class="contentLoaderDot"></div>
|
|
||||||
<div class="contentLoaderDot"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Show question mark if transcription comes back as unknown. -->
|
|
||||||
<div
|
|
||||||
v-else-if="message.content.text === false"
|
|
||||||
class="questionMark"
|
|
||||||
>
|
|
||||||
?
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Else, show text. -->
|
|
||||||
<div v-else>
|
|
||||||
{{ message.content.text }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- message context -->
|
|
||||||
<span
|
|
||||||
v-if="message.isChild === 'last' || message.isChild === 'none'"
|
|
||||||
class="context"
|
|
||||||
>
|
|
||||||
|
|
||||||
<!-- Render Crimata icon or friend's initials. -->
|
|
||||||
<div v-if="message.modifier == 'ai'" class="photo">
|
|
||||||
<img src="@/assets/crimata.svg"/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Render Crimata icon or friend's initials. -->
|
|
||||||
<div v-if="message.modifier == 'fr'" class="photo"
|
|
||||||
:class="{ playing: isPlaying }"
|
|
||||||
>
|
|
||||||
{{ message.context[0] }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{ message.context }}
|
|
||||||
|
|
||||||
</span>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang='ts'>
|
|
||||||
|
|
||||||
import { defineComponent, ref, onMounted } from 'vue';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: "MessageItem",
|
|
||||||
|
|
||||||
props: ["message"],
|
|
||||||
|
|
||||||
setup(props) {
|
|
||||||
|
|
||||||
let addListener = false;
|
|
||||||
|
|
||||||
const notify = ref(false)
|
|
||||||
const isPlaying = ref(false);
|
|
||||||
|
|
||||||
if (!props.message.seen) {
|
|
||||||
console.log("MSG:Checking...")
|
|
||||||
|
|
||||||
// newMessages always render with blue dot.
|
|
||||||
if (props.message.newMessage) {
|
|
||||||
console.log("MSG:New message, notifying.")
|
|
||||||
props.message.seen = true;
|
|
||||||
notify.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
else {
|
|
||||||
|
|
||||||
// Do nothing if window is visible on setup.
|
|
||||||
if (document.visibilityState === "visible") {
|
|
||||||
props.message.seen = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We must listen for window visible event.
|
|
||||||
else {
|
|
||||||
console.log("MSG:Unseen, adding listener.")
|
|
||||||
addListener = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Callback for window visibilityState.
|
|
||||||
const onVisibilityChange = (_event: any) => {
|
|
||||||
if (document.visibilityState === "visible") {
|
|
||||||
console.log("MSG:Window visible, notifying.")
|
|
||||||
|
|
||||||
props.message.seen = true;
|
|
||||||
notify.value = true; // triggers blue dot anim.
|
|
||||||
|
|
||||||
// Once complete we can remove.
|
|
||||||
document.removeEventListener("visibilitychange", onVisibilityChange)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onStopPlayback = (_event: any) => {
|
|
||||||
window.ipcRenderer.removeAllListeners("stop-playback-anim");
|
|
||||||
isPlaying.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
|
|
||||||
// Listen for window focus event.
|
|
||||||
if (addListener) {
|
|
||||||
document.addEventListener("visibilitychange", onVisibilityChange)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Turn on audio playback animation if audio.
|
|
||||||
if (props.message.content.audio === true) {
|
|
||||||
window.ipcRenderer.on("stop-playback-anim", onStopPlayback);
|
|
||||||
isPlaying.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
notify,
|
|
||||||
isPlaying
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
|
|
||||||
.message {
|
|
||||||
width: 100vw;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
padding-top: 9px;
|
|
||||||
padding-bottom: 9px;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.message:first-child {
|
|
||||||
margin-top: 55px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message:last-child {
|
|
||||||
margin-bottom: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.divider {
|
|
||||||
width: 100vw;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
font-family: "SF Compact Display";
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #9B9B9B;
|
|
||||||
|
|
||||||
margin-bottom: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.firstChildMessage {
|
|
||||||
padding-bottom: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.middleChildMessage {
|
|
||||||
padding-top: 2px;
|
|
||||||
padding-bottom: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lastChildMessage {
|
|
||||||
padding-top: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#aiMessage {
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
#sfMessage {
|
|
||||||
align-items: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
#frMessage {
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.messageBox {
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
// Animate on render.
|
|
||||||
animation-name: appear;
|
|
||||||
animation-duration: 0.25s;
|
|
||||||
}
|
|
||||||
|
|
||||||
#aiMessageBox {
|
|
||||||
margin-left: 20px;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
#sfMessageBox {
|
|
||||||
margin-right: 20px;
|
|
||||||
align-items: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
#frMessageBox {
|
|
||||||
margin-left: 20px;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bubble {
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
max-width: 66vw;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
font-family: "SF Pro Text";
|
|
||||||
font-size: 14px;
|
|
||||||
|
|
||||||
padding: 10px;
|
|
||||||
|
|
||||||
border-radius: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes appear {
|
|
||||||
10% {
|
|
||||||
transform: scale(0.3);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#aiBubble {
|
|
||||||
background-color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
#sfBubble {
|
|
||||||
background-color: #58c4fd;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
#frBubble {
|
|
||||||
background-color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sf-firstChildBubble {
|
|
||||||
border-bottom-right-radius: 9px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sf-middleChildBubble {
|
|
||||||
border-top-right-radius: 9px;
|
|
||||||
border-bottom-right-radius: 9px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sf-lastChildBubble {
|
|
||||||
border-top-right-radius: 9px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ai-firstChildBubble, .fr-firstChildBubble {
|
|
||||||
border-bottom-left-radius: 9px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ai-middleChildBubble, .fr-middleChildBubble {
|
|
||||||
border-top-left-radius: 9px;
|
|
||||||
border-bottom-left-radius: 9px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ai-lastChildBubble, .fr-lastChildBubble {
|
|
||||||
border-top-left-radius: 9px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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: notify-anim;
|
|
||||||
animation-duration: 5s;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes notify-anim {
|
|
||||||
0%, 90% {
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: scale(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.context {
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
font-family: "SF Compact Display";
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: bold;
|
|
||||||
margin-top: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.photo {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
margin-right: 5px;
|
|
||||||
|
|
||||||
width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
|
|
||||||
font-size: 14px;
|
|
||||||
|
|
||||||
background-color: white;
|
|
||||||
border-radius: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply for audio message playback.
|
|
||||||
.playing {
|
|
||||||
animation-name: circle1;
|
|
||||||
animation-duration: 2s;
|
|
||||||
animation-iteration-count: infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes circle1 {
|
|
||||||
0%,
|
|
||||||
100% {
|
|
||||||
box-shadow: 0 0 0 0.4em rgba(195, 195, 195, 0.4), 0 0 0 0.25em rgba(195, 195, 195, 0.15);
|
|
||||||
}
|
|
||||||
25% {
|
|
||||||
box-shadow: 0 0 0 0.15em rgba(195, 195, 195, 0.15), 0 0 0 0.4em rgba(195, 195, 195, 0.3);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
box-shadow: 0 0 0 0.55em rgba(195, 195, 195, 0.55), 0 0 0 0.15em rgba(195, 195, 195, 0.05);
|
|
||||||
}
|
|
||||||
75% {
|
|
||||||
box-shadow: 0 0 0 0.25em rgba(195, 195, 195, 0.25), 0 0 0 0.55em rgba(195, 195, 195, 0.45);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.contentLoader {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.contentLoaderDot {
|
|
||||||
width: 5px;
|
|
||||||
height: 5px;
|
|
||||||
|
|
||||||
margin: 2px;
|
|
||||||
border-radius: 2.5px;
|
|
||||||
background-color: #357CA2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.questionMark {
|
|
||||||
font-weight: 900;
|
|
||||||
color: #357CA2;
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,115 +0,0 @@
|
||||||
<template>
|
|
||||||
<InputItem :initials="profile.initials"/>
|
|
||||||
<Settings />
|
|
||||||
|
|
||||||
<div id="recIcon" />
|
|
||||||
|
|
||||||
<!-- List of message bubbles. -->
|
|
||||||
<div id="messenger">
|
|
||||||
<Message
|
|
||||||
v-for="message in messages"
|
|
||||||
:message="message[1]"
|
|
||||||
:key="message[0]"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent, onMounted, onUnmounted } from "vue";
|
|
||||||
import Message from "@/components/message.vue";
|
|
||||||
import InputItem from "@/components/inputItem.vue";
|
|
||||||
import Settings from "@/components/settings.vue";
|
|
||||||
import useMitt from "@/modules/mitt";
|
|
||||||
import useMessages from "@/modules/messages";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: "Messenger",
|
|
||||||
|
|
||||||
props: ["profile", "newMessages"],
|
|
||||||
|
|
||||||
components: {
|
|
||||||
Message,
|
|
||||||
InputItem,
|
|
||||||
Settings
|
|
||||||
},
|
|
||||||
|
|
||||||
setup(props) {
|
|
||||||
|
|
||||||
// Listen for self-messages from inputItem.
|
|
||||||
const { emitter } = useMitt();
|
|
||||||
|
|
||||||
// Handle messages in view.
|
|
||||||
const { messages, prepMessageView, updateMessageView } = useMessages();
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
const crimataId = props.profile.crimataId;
|
|
||||||
console.log(`MSGR:Initializing messenger for ${crimataId}.`)
|
|
||||||
|
|
||||||
// Load the message history.
|
|
||||||
if (crimataId === window.localStorage.getItem("last_usr")) {
|
|
||||||
prepMessageView(props.newMessages)
|
|
||||||
} else {
|
|
||||||
messages.value.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
// New content listeners.
|
|
||||||
emitter.on('self-message', (message) => updateMessageView(message));
|
|
||||||
window.ipcRenderer.on("render-message", (_e: any, payload: any) => {
|
|
||||||
updateMessageView(payload.message)
|
|
||||||
});
|
|
||||||
|
|
||||||
// Save the usr for next time.
|
|
||||||
window.localStorage.setItem("last_usr", crimataId)
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
window.ipcRenderer.removeAllListeners("render-message");
|
|
||||||
window.ipcRenderer.removeAllListeners("annotate-message");
|
|
||||||
emitter.all.clear();
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
messages
|
|
||||||
};
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
|
|
||||||
#messenger {
|
|
||||||
width: 100vw;
|
|
||||||
height: 100vh;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
#messenger::-webkit-scrollbar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#recIcon {
|
|
||||||
position: fixed;
|
|
||||||
right: 0;
|
|
||||||
|
|
||||||
opacity: 0;
|
|
||||||
transform: scale(0.0);
|
|
||||||
|
|
||||||
margin-top: 24px;
|
|
||||||
margin-right: 66px;
|
|
||||||
|
|
||||||
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background-color: #FF3B3B;
|
|
||||||
|
|
||||||
z-index: 2;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
<template>
|
|
||||||
|
|
||||||
<input
|
|
||||||
id="textInput"
|
|
||||||
type="text"
|
|
||||||
/>
|
|
||||||
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent } from "vue";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: "TextInput",
|
|
||||||
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
|
|
||||||
#textInput {
|
|
||||||
position: absolute;
|
|
||||||
|
|
||||||
opacity: 0;
|
|
||||||
|
|
||||||
min-width: 150px;
|
|
||||||
height: 16px;
|
|
||||||
|
|
||||||
border-radius: 18px;
|
|
||||||
|
|
||||||
padding: 10px;
|
|
||||||
margin-right: 10px;
|
|
||||||
margin-left: 10px;
|
|
||||||
|
|
||||||
outline: none;
|
|
||||||
border: none;
|
|
||||||
pointer-events: none;
|
|
||||||
|
|
||||||
background-color: white;
|
|
||||||
|
|
||||||
z-index: -1;
|
|
||||||
|
|
||||||
box-shadow: 0px 0px 10px 0px rgba(0,0,0,0.15);
|
|
||||||
|
|
||||||
transform: translateX(50px) scale(0.3);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
||||||
31
src/composables/autoUpdate.ts
Normal file
31
src/composables/autoUpdate.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
const { autoUpdater } = require('electron-updater');
|
||||||
|
|
||||||
|
let win: boolean;
|
||||||
|
|
||||||
|
// Listen for window creation.
|
||||||
|
backgroundMitt.on('window-active', (state: boolean) => {
|
||||||
|
win = state;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auto updating.
|
||||||
|
autoUpdater.requestHeaders = { 'PRIVATE-TOKEN': 'mvvgWYwWnot4bisiQMh_' }
|
||||||
|
|
||||||
|
autoUpdater.on('update-available', (info: any) => {
|
||||||
|
console.log(`Update available: ${info.version}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
autoUpdater.on('update-downloaded', (info: any) => {
|
||||||
|
|
||||||
|
const updateDialog = {
|
||||||
|
type: 'info',
|
||||||
|
buttons: ['Restart', 'Later'],
|
||||||
|
title: 'Application Update',
|
||||||
|
message: info.version,
|
||||||
|
detail: 'A new version has been downloaded. Restart the application to apply the updates.'
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog.showMessageBox(updateDialog).then((returnValue) => {
|
||||||
|
if (returnValue.response === 0) autoUpdater.quitAndInstall()
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
|
@ -1,13 +1,16 @@
|
||||||
/* eslint-disable */
|
|
||||||
|
|
||||||
import { Emitter } from "mitt";
|
|
||||||
|
|
||||||
// Backend emitter
|
// Backend emitter
|
||||||
|
//
|
||||||
type Mitt = Emitter;
|
|
||||||
|
|
||||||
const EventEmitter = require('events');
|
const EventEmitter = require('events');
|
||||||
|
|
||||||
class BackgroundMitt extends EventEmitter { }
|
class BackgroundMitt extends EventEmitter { }
|
||||||
|
|
||||||
export const backgroundMitt = new BackgroundMitt();
|
export const backgroundMitt = new BackgroundMitt();
|
||||||
|
|
||||||
|
export const ipcEmit = <T>(channel: string, payload: T) => {
|
||||||
|
backgroundMitt.emit('ipc-renderer', {
|
||||||
|
channel,
|
||||||
|
payload
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
51
src/composables/useHttp.ts
Normal file
51
src/composables/useHttp.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
|
||||||
|
import axios, { AxiosRequestConfig } from 'axios';
|
||||||
|
import {config} from "@/config";
|
||||||
|
|
||||||
|
const baseURL = config.BUSINESS_URL + config.BUSINESS_PREFIX;
|
||||||
|
|
||||||
|
interface Request {
|
||||||
|
endpoint: string;
|
||||||
|
query?: Record<string, any>;
|
||||||
|
config?: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const makeQuery = (reqQuery: Record<string, any>) => {
|
||||||
|
|
||||||
|
let result = '';
|
||||||
|
|
||||||
|
result = '?' + Object.entries(reqQuery)
|
||||||
|
.map(([ key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
|
||||||
|
.join('&')
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export default function useHttp() {
|
||||||
|
|
||||||
|
const api = axios.create({
|
||||||
|
baseURL,
|
||||||
|
withCredentials: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const post = async (endpoint: string, payload?: Record<string, any>): Promise<any> => (
|
||||||
|
await api.post(endpoint, payload)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
const get = async (req: Request) => {
|
||||||
|
|
||||||
|
if (req.query) {
|
||||||
|
req.endpoint += makeQuery(req.query);
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await api.get(req.endpoint, req.config);
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
get, post
|
||||||
|
}
|
||||||
|
}
|
||||||
82
src/composables/useIpcMain.ts
Normal file
82
src/composables/useIpcMain.ts
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
|
||||||
|
import { ipcMain, IpcMainInvokeEvent, IpcMainEvent } from "electron";
|
||||||
|
|
||||||
|
export class IpcHandler<InputParam, ReturnType> implements IIpcHandler<InputParam, ReturnType> {
|
||||||
|
|
||||||
|
readonly channel: string;
|
||||||
|
|
||||||
|
readonly _handlerCallback: IpcHandlerCallback<InputParam, ReturnType>;
|
||||||
|
|
||||||
|
constructor(options: {
|
||||||
|
channel: string;
|
||||||
|
handlerCallback: IpcHandlerCallback<InputParam, ReturnType>;
|
||||||
|
}) {
|
||||||
|
this.channel = options.channel;
|
||||||
|
this._handlerCallback = options.handlerCallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
handle() {
|
||||||
|
this.remove();
|
||||||
|
ipcMain.handle(this.channel, this._onInvoke);
|
||||||
|
}
|
||||||
|
|
||||||
|
remove() {
|
||||||
|
ipcMain.removeHandler(this.channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _onInvoke = (_e: IpcMainInvokeEvent, payload?: string | null): Promise<Error | ReturnType> => {
|
||||||
|
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
|
||||||
|
console.log(`[IPC] Handle:${this.channel}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
const params = payload ? JSON.parse(payload) : null;
|
||||||
|
|
||||||
|
const res = await this._handlerCallback(params);
|
||||||
|
|
||||||
|
resolve(res as unknown as ReturnType);
|
||||||
|
|
||||||
|
} catch(e) {
|
||||||
|
console.log(`[IPC] Error:${this.channel}`);
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export class IpcListener<InputParam> implements IIpcListener<InputParam> {
|
||||||
|
|
||||||
|
readonly channel: string;
|
||||||
|
|
||||||
|
readonly _listenerCallback: IpcListenerCallback<InputParam>;
|
||||||
|
|
||||||
|
constructor(options: {
|
||||||
|
channel: string;
|
||||||
|
listenerCallback: IpcListenerCallback<InputParam>;
|
||||||
|
}) {
|
||||||
|
this.channel = options.channel;
|
||||||
|
this._listenerCallback = options.listenerCallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
listen() {
|
||||||
|
this.remove();
|
||||||
|
ipcMain.on(this.channel, this._onPost);
|
||||||
|
}
|
||||||
|
|
||||||
|
remove() {
|
||||||
|
ipcMain.removeAllListeners(this.channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _onPost = (_e: IpcMainEvent, payload?: string | null): void => {
|
||||||
|
|
||||||
|
console.log(`[IPC] Post: ${this.channel}`);
|
||||||
|
|
||||||
|
const params = payload ? JSON.parse(payload) : null;
|
||||||
|
this._listenerCallback(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
36
src/composables/useMessageCanvas.ts
Normal file
36
src/composables/useMessageCanvas.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export default class Canvas {
|
||||||
|
|
||||||
|
messages: Message[];
|
||||||
|
|
||||||
|
/* seed canvas with messages on init */
|
||||||
|
constructor(messages: Message[]) {
|
||||||
|
this.messages = messages;
|
||||||
|
ipcEmit("seed-view", this.messages);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* add a new message to the canvas */
|
||||||
|
add(message: Message) {
|
||||||
|
this.messages.push(message);
|
||||||
|
ipcEmit("update-view", message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* update an existing message */
|
||||||
|
update(message: Message) {
|
||||||
|
|
||||||
|
/* get the target message */
|
||||||
|
let target_message = this.messages.filter((m: Message) => {
|
||||||
|
return m.uid = message.uid;
|
||||||
|
})[0];
|
||||||
|
|
||||||
|
/* replace the target message */
|
||||||
|
if (target_message) {
|
||||||
|
target_message = message;
|
||||||
|
ipcEmit("update-view", message);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
13
src/composables/useSaveToJSON.ts
Normal file
13
src/composables/useSaveToJSON.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
|
||||||
|
import {config} from "@/config";
|
||||||
|
import fs from 'fs';
|
||||||
|
|
||||||
|
export const saveToJson = (fileName: string, data: any) => {
|
||||||
|
|
||||||
|
fs.writeFile(config.configPath + fileName, JSON.stringify(data), (err) => {
|
||||||
|
if (err) {
|
||||||
|
console.log("Error when saving to json.")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
89
src/composables/useWebsockets.ts
Normal file
89
src/composables/useWebsockets.ts
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
import WebSocket from 'ws';
|
||||||
|
|
||||||
|
|
||||||
|
const _connectionCheckTimeout = 4000;
|
||||||
|
const _reconnectTimeout = 1000;
|
||||||
|
let _connectionCheckInterval: ReturnType<typeof setTimeout>;
|
||||||
|
|
||||||
|
|
||||||
|
export default function useWebSockets(
|
||||||
|
messageCallback: (message: string) => void,
|
||||||
|
connectionStatusCallback: (alive: boolean) => void,
|
||||||
|
) {
|
||||||
|
|
||||||
|
let socket: WebSocket;
|
||||||
|
|
||||||
|
const send = async (data: Record<string, any>): Promise<boolean> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (socket) {
|
||||||
|
if (socket.readyState === WebSocket.OPEN) {
|
||||||
|
socket.send(JSON.stringify(data));
|
||||||
|
resolve(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reject(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const connect = (socketUrl: string, secret: string) => {
|
||||||
|
|
||||||
|
// avoid setting multiple interval;
|
||||||
|
if (_connectionCheckInterval) clearInterval(_connectionCheckInterval);
|
||||||
|
|
||||||
|
/* create a new socket */
|
||||||
|
socket = new WebSocket(socketUrl);
|
||||||
|
|
||||||
|
/* add event listeners */
|
||||||
|
socket.on("open", () => {
|
||||||
|
|
||||||
|
socket.send(JSON.stringify({key: secret}));
|
||||||
|
|
||||||
|
// ping server
|
||||||
|
_connectionCheckInterval = setInterval(() => {
|
||||||
|
|
||||||
|
socket.ping(null, true, (e: Error) => {
|
||||||
|
if (e) {
|
||||||
|
socket.close();
|
||||||
|
connectionStatusCallback(false);
|
||||||
|
setTimeout(() => connect(socketUrl, secret), _reconnectTimeout);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}, _connectionCheckTimeout);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("message", (event: WebSocket.MessageEvent) => {
|
||||||
|
console.log("message received", event);
|
||||||
|
messageCallback(event.toString())
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("close", (event: WebSocket.CloseEvent) => {
|
||||||
|
connectionStatusCallback(false);
|
||||||
|
clearInterval(_connectionCheckInterval);
|
||||||
|
if (!event.wasClean) {
|
||||||
|
setTimeout(() => connect(socketUrl, secret), _reconnectTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("pong", () => connectionStatusCallback(true));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
if (socket) {
|
||||||
|
socket.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
connect,
|
||||||
|
send,
|
||||||
|
close
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
17
src/config.ts
Normal file
17
src/config.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
|
||||||
|
import { app } from "electron";
|
||||||
|
|
||||||
|
const env = process.env;
|
||||||
|
|
||||||
|
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';
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
PLATFORM_URL: `${PLATFORM_IP}:${PLATFORM_PORT}`,
|
||||||
|
BUSINESS_URL: `${BUSINESS_IP}:${BUSINESS_PORT}`,
|
||||||
|
BUSINESS_PREFIX: '/api',
|
||||||
|
configPath: app.getPath('userData')
|
||||||
|
}
|
||||||
52
src/init.ts
Normal file
52
src/init.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
/**
|
||||||
|
* Entry point for Crimata electron app.
|
||||||
|
* "Look on my Works, ye Mighty, and despair!"
|
||||||
|
*/
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
import { app, protocol } from "electron";
|
||||||
|
import createWindow from "./window";
|
||||||
|
import main from "./main";
|
||||||
|
import { backgroundMitt } from '@/composables/useEmitter';
|
||||||
|
|
||||||
|
console.log('Starting Crimata electron app.');
|
||||||
|
|
||||||
|
// Scheme must be registered before the app is ready
|
||||||
|
protocol.registerSchemesAsPrivileged([
|
||||||
|
{ scheme: "app", privileges: { secure: true, standard: true } }
|
||||||
|
]);
|
||||||
|
|
||||||
|
const isDev = require('electron-is-dev');
|
||||||
|
|
||||||
|
let win: boolean;
|
||||||
|
|
||||||
|
// Listen for window creation.
|
||||||
|
backgroundMitt.on('window-active', (state: boolean) => {
|
||||||
|
win = state;
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Start main process on ready */
|
||||||
|
app.on("ready", async () => {
|
||||||
|
await main();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Must keep to ensure app doesn't quit on close.
|
||||||
|
app.on("before-quit", async () => {
|
||||||
|
});
|
||||||
|
|
||||||
|
// Must keep to ensure app doesn't quit on close.
|
||||||
|
app.on("window-all-closed", () => {
|
||||||
|
});
|
||||||
|
|
||||||
|
// When user clicks app icon (re-open)
|
||||||
|
app.on("activate", () => {
|
||||||
|
if (!win) createWindow();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Exit cleanly on request from parent process in development mode.
|
||||||
|
if (isDev) {
|
||||||
|
process.on("SIGTERM", () => {
|
||||||
|
app.quit();
|
||||||
|
});
|
||||||
|
}
|
||||||
28
src/ipc/handlers.ts
Normal file
28
src/ipc/handlers.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
import { accountLogin, accountLogout, accountProfile } from "@/account";
|
||||||
|
import { IpcHandler } from "@/composables/useIpcMain";
|
||||||
|
import { flush } from "@/audio";
|
||||||
|
|
||||||
|
|
||||||
|
const LOGIN_CHANNEL = "invoke-account-login";
|
||||||
|
const LOGOUT_CHANNEL = "invoke-account-logout";
|
||||||
|
const GET_AUDIO_CHANNEL = "invoke-audio-flush";
|
||||||
|
|
||||||
|
export const loginHandler = new IpcHandler({
|
||||||
|
channel: LOGIN_CHANNEL,
|
||||||
|
handlerCallback: accountLogin
|
||||||
|
});
|
||||||
|
|
||||||
|
export const logoutHandler = new IpcHandler({
|
||||||
|
channel: LOGOUT_CHANNEL,
|
||||||
|
handlerCallback: accountLogout
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getAudioHandler = new IpcHandler({
|
||||||
|
channel: GET_AUDIO_CHANNEL,
|
||||||
|
handlerCallback: flush
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
33
src/ipc/index.ts
Normal file
33
src/ipc/index.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
import * as handlers from "./handlers";
|
||||||
|
import * as listeners from "./listeners";
|
||||||
|
|
||||||
|
const ipcHandlers: IPCHandlers = {};
|
||||||
|
const ipcListeners: IPCListeners = {};
|
||||||
|
|
||||||
|
const _initHandlers = (): void => {
|
||||||
|
for (const [key, handler] of Object.entries(handlers)) {
|
||||||
|
if (!(key in ipcHandlers)) {
|
||||||
|
ipcHandlers[key] = handler;
|
||||||
|
handler.handle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const _initListeners = (): void => {
|
||||||
|
for (const [key, listener] of Object.entries(listeners)) {
|
||||||
|
if (!(key in ipcListeners)) {
|
||||||
|
ipcListeners[key] = listener;
|
||||||
|
listener.listen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function initIpcMain(): void {
|
||||||
|
_initHandlers();
|
||||||
|
_initListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
24
src/ipc/listeners.ts
Normal file
24
src/ipc/listeners.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
|
||||||
|
import { IpcListener } from "@/composables/useIpcMain"
|
||||||
|
import { sendMessage } from '@/session';
|
||||||
|
import { collect } from "@/audio";
|
||||||
|
import { updateAppState } from "@/account";
|
||||||
|
|
||||||
|
const CLIENT_MESSAGE_CHANNEL = "post-session-send"
|
||||||
|
const GET_AUDIO_CHANNEL = "post-audio-collect";
|
||||||
|
const APP_MOUNT_CHANNEL = "post-app-mount";
|
||||||
|
|
||||||
|
export const messageListener = new IpcListener<Message>({
|
||||||
|
channel: CLIENT_MESSAGE_CHANNEL,
|
||||||
|
listenerCallback: sendMessage
|
||||||
|
});
|
||||||
|
|
||||||
|
export const audioChunkListener = new IpcListener<ArrayBuffer>({
|
||||||
|
channel: GET_AUDIO_CHANNEL,
|
||||||
|
listenerCallback: collect
|
||||||
|
});
|
||||||
|
|
||||||
|
export const appMountListener = new IpcListener<null>({
|
||||||
|
channel: APP_MOUNT_CHANNEL,
|
||||||
|
listenerCallback: updateAppState
|
||||||
|
});
|
||||||
42
src/main.ts
42
src/main.ts
|
|
@ -1,15 +1,39 @@
|
||||||
// src/main.ts
|
/**
|
||||||
|
* 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 App from "./App.vue";
|
import initIpcMain from "@/ipc/index";
|
||||||
|
import { accountAuth, updateAppState } from "./account";
|
||||||
|
import { launchSession } from "./session";
|
||||||
|
import createWindow from "./window";
|
||||||
|
|
||||||
import mitt from "mitt";
|
let authState: AuthState | null;
|
||||||
import { createApp } from "vue";
|
|
||||||
|
|
||||||
|
export default async function main() {
|
||||||
|
|
||||||
// Handle events.
|
/* initiate controls for frontend to use when needed */
|
||||||
const emitter = mitt();
|
initIpcMain();
|
||||||
|
|
||||||
const app = createApp(App)
|
/* launch browser window */
|
||||||
|
await createWindow();
|
||||||
|
|
||||||
app.provide("mitt", emitter)
|
try {
|
||||||
app.mount("#app");
|
authState = await accountAuth() as AuthState;
|
||||||
|
} catch(e) {
|
||||||
|
console.log('AUTH:', e);
|
||||||
|
authState = null;
|
||||||
|
} finally {
|
||||||
|
if (authState) {
|
||||||
|
launchSession(authState.token as string);
|
||||||
|
}
|
||||||
|
updateAppState();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
|
|
||||||
export const useIpc = () => {
|
|
||||||
|
|
||||||
const invoke = async (endpoint: string, payload: any) => {
|
|
||||||
try {
|
|
||||||
const res = await window.ipcRenderer.invoke(endpoint, payload);
|
|
||||||
return res;
|
|
||||||
} catch (e) {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const post = (endpoint: string, payload: any) => {
|
|
||||||
window.ipcRenderer.send(endpoint, payload);
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
invoke,
|
|
||||||
post
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,62 +0,0 @@
|
||||||
import {
|
|
||||||
RenderMessage,
|
|
||||||
ClientMessage,
|
|
||||||
ClientRequest,
|
|
||||||
AuthRequest,
|
|
||||||
LogoutRequest
|
|
||||||
} from "@/types";
|
|
||||||
|
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
|
||||||
|
|
||||||
function getTimeStamp(): number {
|
|
||||||
const currentdate = new Date();
|
|
||||||
return currentdate.getTime();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a RenderMessage object.
|
|
||||||
export const renderMessage = (text: boolean | string, audio: boolean | string, context: string, modifier: string): RenderMessage => (
|
|
||||||
{
|
|
||||||
content: {
|
|
||||||
text: text,
|
|
||||||
audio: audio
|
|
||||||
},
|
|
||||||
context: context,
|
|
||||||
modifier: modifier,
|
|
||||||
time: getTimeStamp(),
|
|
||||||
uid: uuidv4(),
|
|
||||||
isChild: "none",
|
|
||||||
seen: false,
|
|
||||||
newMessage: false
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
export const clientMessage = (text: string, audio: string | boolean, uid: string): ClientMessage => (
|
|
||||||
{
|
|
||||||
audio,
|
|
||||||
text,
|
|
||||||
uid
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
export const clientRequest = (intent: string, params: object, epic: string | boolean): ClientRequest => (
|
|
||||||
{
|
|
||||||
intent: intent,
|
|
||||||
params: params,
|
|
||||||
epic: epic,
|
|
||||||
confidence: 1.0
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
export const authRequest = (key: boolean | string, usr: boolean | string, pwd: boolean | string): AuthRequest => (
|
|
||||||
{
|
|
||||||
key,
|
|
||||||
usr,
|
|
||||||
pwd
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
export const logoutRequest = (): LogoutRequest => (
|
|
||||||
{
|
|
||||||
logout: true
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
@ -1,125 +0,0 @@
|
||||||
import { ref } from 'vue';
|
|
||||||
import useScroll from "@/modules/scroll";
|
|
||||||
import { RenderMessage, Annotation } from "@/types";
|
|
||||||
|
|
||||||
const messages = ref(new Map());
|
|
||||||
|
|
||||||
const addMessage = (message: RenderMessage) => {
|
|
||||||
messages.value.set(message.uid, message)
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateMessage = (annotation: Annotation) => {
|
|
||||||
const message = messages.value.get(annotation.uid)
|
|
||||||
message.context = annotation.context
|
|
||||||
message.content.text = annotation.text
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadSavedMessages = () => {
|
|
||||||
const rawData = window.localStorage.getItem("crimata_messages");
|
|
||||||
if (rawData) {
|
|
||||||
const messageData = JSON.parse(rawData)
|
|
||||||
messages.value = new Map(Object.entries(messageData));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const saveMessages = () => {
|
|
||||||
const messageData = Object.fromEntries(messages.value);
|
|
||||||
window.localStorage.setItem("crimata_messages", JSON.stringify(messageData));
|
|
||||||
}
|
|
||||||
|
|
||||||
const isSimmilar = (messageA: RenderMessage, messageB: RenderMessage) => {
|
|
||||||
if ((Math.abs(messageA.time - messageB.time) < 20000) && (messageA.modifier == messageB.modifier) && (messageA.context == messageB.context)) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateGrouping = () => {
|
|
||||||
console.log("updating grouping")
|
|
||||||
const refs = Array.from(messages.value.keys())
|
|
||||||
|
|
||||||
// Get the last three messages.
|
|
||||||
const first = messages.value.get(refs[refs.length - 1])
|
|
||||||
const second = messages.value.get(refs[refs.length - 2])
|
|
||||||
const third = messages.value.get(refs[refs.length - 3])
|
|
||||||
|
|
||||||
// If messages are simmilar, update the classes.
|
|
||||||
if ((first) && (second)) {
|
|
||||||
if (isSimmilar(first, second)) {
|
|
||||||
first.isChild = "last" // i.e. last in group.
|
|
||||||
second.isChild = "first"
|
|
||||||
|
|
||||||
if (third) {
|
|
||||||
if ((third.isChild == "first") || (third.isChild == "middle")) {
|
|
||||||
second.isChild = "middle"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export default function useMessages() {
|
|
||||||
|
|
||||||
// Scroll controller.
|
|
||||||
const { setScroll, updateScrollRef, adjustScroll } = useScroll("messenger");
|
|
||||||
|
|
||||||
// Main function for updating the message view.
|
|
||||||
const updateMessageView = (message: RenderMessage | Annotation) => {
|
|
||||||
|
|
||||||
// Step 1: See if user is scrolled down.
|
|
||||||
updateScrollRef()
|
|
||||||
|
|
||||||
// Step 2: Add the new content to the view.
|
|
||||||
if ("content" in message) {
|
|
||||||
addMessage(message)
|
|
||||||
} else {
|
|
||||||
updateMessage(message)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 3: Pop off oldest message (if > 200).
|
|
||||||
if (messages.value.size >= 200) {
|
|
||||||
const oldest = Array.from(messages.value.keys()).shift();
|
|
||||||
messages.value.delete(oldest);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 4: Update grouping.
|
|
||||||
updateGrouping()
|
|
||||||
|
|
||||||
// Setp 5: Scroll the view (if scrolled down).
|
|
||||||
setTimeout(adjustScroll, 20);
|
|
||||||
|
|
||||||
// Step 6: Save the view data.
|
|
||||||
saveMessages()
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Seed message view with message history.
|
|
||||||
const prepMessageView = (newMessages: RenderMessage[]) => {
|
|
||||||
console.log("MSGR:Prepping messenger view.")
|
|
||||||
|
|
||||||
// Load and render saved messages and immediately scroll to bottom.
|
|
||||||
loadSavedMessages()
|
|
||||||
setTimeout(setScroll.bind(false), 10);
|
|
||||||
|
|
||||||
// Render new messages, then wait 1s to scroll.
|
|
||||||
if (newMessages.length) {
|
|
||||||
|
|
||||||
console.log("MSGR:Adding new messages")
|
|
||||||
|
|
||||||
newMessages.forEach(message => {
|
|
||||||
message.newMessage = true;
|
|
||||||
addMessage(message)
|
|
||||||
})
|
|
||||||
|
|
||||||
setTimeout(setScroll.bind(true), 1000);
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
messages,
|
|
||||||
prepMessageView,
|
|
||||||
updateMessageView
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
import { inject } from "vue";
|
|
||||||
import { Emitter } from "mitt";
|
|
||||||
|
|
||||||
// Frontend emitter
|
|
||||||
|
|
||||||
type Mitt = Emitter;
|
|
||||||
|
|
||||||
let emitter: Mitt;
|
|
||||||
|
|
||||||
export default function useMitt() {
|
|
||||||
|
|
||||||
const emitterInject: Mitt | undefined = inject("mitt");
|
|
||||||
if (emitterInject) {
|
|
||||||
emitter = emitterInject;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
emitter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
|
|
||||||
|
|
||||||
export default function useScroll(element: string) {
|
|
||||||
|
|
||||||
let isScrolledToBottom: boolean;
|
|
||||||
|
|
||||||
// Set the initial scroll position.
|
|
||||||
const setScroll = (smooth: boolean) => {
|
|
||||||
const view = document.getElementById(element)
|
|
||||||
|
|
||||||
if (view) {
|
|
||||||
view.scrollTo({
|
|
||||||
top: view.scrollHeight - view.clientHeight,
|
|
||||||
behavior: (smooth) ? 'smooth' : 'auto'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update isScrolledToBottom
|
|
||||||
const updateScrollRef = () => {
|
|
||||||
const view = document.getElementById(element)
|
|
||||||
|
|
||||||
if (view) {
|
|
||||||
isScrolledToBottom = view.scrollHeight - view.clientHeight <= view.scrollTop + 1
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Adjust scroll after we add content to the messenger.
|
|
||||||
const adjustScroll = () => {
|
|
||||||
const view = document.getElementById(element)
|
|
||||||
|
|
||||||
if (view) {
|
|
||||||
if (isScrolledToBottom) {
|
|
||||||
view.scrollTo({
|
|
||||||
top: view.scrollHeight - view.clientHeight,
|
|
||||||
behavior: 'smooth'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
updateScrollRef,
|
|
||||||
adjustScroll,
|
|
||||||
setScroll,
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,72 +0,0 @@
|
||||||
"use strict";
|
|
||||||
|
|
||||||
import WebSocket from 'ws';
|
|
||||||
import { ipcMain } from "electron";
|
|
||||||
|
|
||||||
let socket: WebSocket;
|
|
||||||
|
|
||||||
|
|
||||||
// Run every time we want to connect to backend.
|
|
||||||
export default function useWebSockets(receiveCallback: (s: string) => any, openCallback: () => any) {
|
|
||||||
|
|
||||||
// Returns bool (sucess or fail).
|
|
||||||
const sendMessage = (data: any) => {
|
|
||||||
console.log("WS:Sending message: ", data)
|
|
||||||
|
|
||||||
if (socket.readyState !== 1) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
else {
|
|
||||||
socket.send(JSON.stringify(data))
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
const onOpen = (event: WebSocket.OpenEvent) => {
|
|
||||||
|
|
||||||
console.log("WS:Connected to WS Server!");
|
|
||||||
|
|
||||||
openCallback()
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
const onServerMessage = (event: WebSocket.MessageEvent) => {
|
|
||||||
|
|
||||||
console.log("WS:Message received: ", event.data);
|
|
||||||
|
|
||||||
receiveCallback(event.data.toString())
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
const onClose = (event: WebSocket.CloseEvent) => {
|
|
||||||
console.log("WS:Socket closed normally.")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reconnect automatically on error.
|
|
||||||
const onError = (event: WebSocket.ErrorEvent) => {
|
|
||||||
console.log("WS:WebSocket error: ", event.message);
|
|
||||||
|
|
||||||
console.log("Attempting reconnect in 1s.")
|
|
||||||
setTimeout(createSocket, 1000)
|
|
||||||
}
|
|
||||||
|
|
||||||
const createSocket = () => {
|
|
||||||
socket = new WebSocket(`ws://127.0.0.1:8760`)
|
|
||||||
|
|
||||||
// Add listeners.
|
|
||||||
socket.addEventListener("open", onOpen)
|
|
||||||
socket.addEventListener("message", onServerMessage)
|
|
||||||
socket.addEventListener("close", onClose)
|
|
||||||
socket.addEventListener("error", onError)
|
|
||||||
|
|
||||||
console.log("WS:New socket created.")
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
createSocket,
|
|
||||||
sendMessage
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
76
src/render/App.vue
Normal file
76
src/render/App.vue
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
<template>
|
||||||
|
<!-- Only render when profile has been set -->
|
||||||
|
<main id="app" v-if="authComplete">
|
||||||
|
|
||||||
|
<Header />
|
||||||
|
|
||||||
|
<Messenger
|
||||||
|
v-if="profile"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Main Components
|
||||||
|
-->
|
||||||
|
<Login v-else />
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- Show spash screen if ready is false -->
|
||||||
|
<Splash v-else />
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
|
||||||
|
import { defineComponent, onMounted } from "vue";
|
||||||
|
import { IpcRendererEvent } from "electron";
|
||||||
|
|
||||||
|
import Splash from "@/render/components/splash.vue";
|
||||||
|
import Messenger from "@/render/components/messenger.vue";
|
||||||
|
import Login from "@/render/components/login.vue";
|
||||||
|
import Header from "@/render/components/header.vue";
|
||||||
|
|
||||||
|
import { profile, authComplete } from "@/render/composables/useProfile";
|
||||||
|
import { postAppMount } from "@/render/ipc";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
|
||||||
|
components: {
|
||||||
|
Splash,
|
||||||
|
Messenger,
|
||||||
|
Login,
|
||||||
|
Header
|
||||||
|
},
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
|
||||||
|
onMounted(() => postAppMount());
|
||||||
|
|
||||||
|
return {
|
||||||
|
authComplete,
|
||||||
|
profile
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
font-family: "SF Pro Text";
|
||||||
|
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
|
||||||
|
border-radius: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
331
src/render/components/bubble.vue
Normal file
331
src/render/components/bubble.vue
Normal file
|
|
@ -0,0 +1,331 @@
|
||||||
|
<template>
|
||||||
|
<div :class="`${type}-message`">
|
||||||
|
|
||||||
|
<!-- message bubble -->
|
||||||
|
<div :class="`${type}-${child}-bubble`">
|
||||||
|
<div class="notification-dot"/>
|
||||||
|
<div class="error-dot"/>
|
||||||
|
{{ text }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- message context -->
|
||||||
|
<span class="context">
|
||||||
|
<div :class="`${type}-icon`"/>
|
||||||
|
{{ context }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang='ts'>
|
||||||
|
|
||||||
|
import { defineComponent, ref, onMounted } from 'vue';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: "Bubble",
|
||||||
|
|
||||||
|
props: ["text", "context", "type", "position"],
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
|
||||||
|
const seen = ref(false);
|
||||||
|
/* initialize seen state */
|
||||||
|
if (document.visibilityState === "visible") {
|
||||||
|
seen.value = true;
|
||||||
|
} else seen.value = false;
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
|
||||||
|
if(seen.value)
|
||||||
|
document.addEventListener("visibilitychange", () => {
|
||||||
|
seen.value = true
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
seen
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
|
.message {
|
||||||
|
width: 100vw;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding-top: 9px;
|
||||||
|
padding-bottom: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message:first-child {
|
||||||
|
margin-top: 55px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message:last-child {
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-message {
|
||||||
|
@extend .message;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-message, .fr-message {
|
||||||
|
@extend .message;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bubble {
|
||||||
|
position: relative;
|
||||||
|
max-width: 66vw;
|
||||||
|
font-family: "SF Pro Text";
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-bubble {
|
||||||
|
@extend .bubble;
|
||||||
|
background-color: #58c4fd;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.firstChildMessage {
|
||||||
|
padding-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.middleChildMessage {
|
||||||
|
padding-top: 2px;
|
||||||
|
padding-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lastChildMessage {
|
||||||
|
padding-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#aiMessage {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sfMessage {
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
#frMessage {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messageBox {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
// Animate on render.
|
||||||
|
animation-name: appear;
|
||||||
|
animation-duration: 0.25s;
|
||||||
|
}
|
||||||
|
|
||||||
|
#aiMessageBox {
|
||||||
|
margin-left: 20px;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sfMessageBox {
|
||||||
|
margin-right: 20px;
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
#frMessageBox {
|
||||||
|
margin-left: 20px;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bubble {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
max-width: 66vw;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
font-family: "SF Pro Text";
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
padding: 10px;
|
||||||
|
|
||||||
|
border-radius: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes appear {
|
||||||
|
10% {
|
||||||
|
transform: scale(0.3);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#aiBubble {
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sfBubble {
|
||||||
|
background-color: #58c4fd;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
#frBubble {
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-firstChildBubble {
|
||||||
|
border-bottom-right-radius: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-middleChildBubble {
|
||||||
|
border-top-right-radius: 9px;
|
||||||
|
border-bottom-right-radius: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-lastChildBubble {
|
||||||
|
border-top-right-radius: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-firstChildBubble, .fr-firstChildBubble {
|
||||||
|
border-bottom-left-radius: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-middleChildBubble, .fr-middleChildBubble {
|
||||||
|
border-top-left-radius: 9px;
|
||||||
|
border-bottom-left-radius: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-lastChildBubble, .fr-lastChildBubble {
|
||||||
|
border-top-left-radius: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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: notify-anim;
|
||||||
|
animation-duration: 5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes notify-anim {
|
||||||
|
0%, 90% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.context {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
font-family: "SF Compact Display";
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
margin-right: 5px;
|
||||||
|
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply for audio message playback.
|
||||||
|
.playing {
|
||||||
|
animation-name: circle1;
|
||||||
|
animation-duration: 2s;
|
||||||
|
animation-iteration-count: infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes circle1 {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
box-shadow: 0 0 0 0.4em rgba(195, 195, 195, 0.4), 0 0 0 0.25em rgba(195, 195, 195, 0.15);
|
||||||
|
}
|
||||||
|
25% {
|
||||||
|
box-shadow: 0 0 0 0.15em rgba(195, 195, 195, 0.15), 0 0 0 0.4em rgba(195, 195, 195, 0.3);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
box-shadow: 0 0 0 0.55em rgba(195, 195, 195, 0.55), 0 0 0 0.15em rgba(195, 195, 195, 0.05);
|
||||||
|
}
|
||||||
|
75% {
|
||||||
|
box-shadow: 0 0 0 0.25em rgba(195, 195, 195, 0.25), 0 0 0 0.55em rgba(195, 195, 195, 0.45);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.contentLoader {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contentLoaderDot {
|
||||||
|
width: 5px;
|
||||||
|
height: 5px;
|
||||||
|
|
||||||
|
margin: 2px;
|
||||||
|
border-radius: 2.5px;
|
||||||
|
background-color: #357CA2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.questionMark {
|
||||||
|
font-weight: 900;
|
||||||
|
color: #357CA2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// .divider {
|
||||||
|
// width: 100vw;
|
||||||
|
// display: flex;
|
||||||
|
// justify-content: center;
|
||||||
|
// align-items: center;
|
||||||
|
|
||||||
|
// font-family: "SF Compact Display";
|
||||||
|
// font-size: 12px;
|
||||||
|
// font-weight: bold;
|
||||||
|
// color: #9B9B9B;
|
||||||
|
|
||||||
|
// margin-bottom: 18px;
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
0
src/render/components/controllers/bubble.control.ts
Normal file
0
src/render/components/controllers/bubble.control.ts
Normal file
102
src/render/components/controllers/helpers.ts
Normal file
102
src/render/components/controllers/helpers.ts
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
import { ref } from "vue";
|
||||||
|
import anime from "animejs";
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
|
|
||||||
|
export function animateTextInput () {
|
||||||
|
|
||||||
|
const side = ref("right");
|
||||||
|
|
||||||
|
function show () {
|
||||||
|
const t1 = (side.value === "right") ? 50 : -70;
|
||||||
|
const t2 = (side.value === "right") ? 110 : -130;
|
||||||
|
|
||||||
|
anime({
|
||||||
|
targets: '#textInput',
|
||||||
|
opacity: [0, 1],
|
||||||
|
translateX: [t1, t2],
|
||||||
|
scale: [0.3, 1],
|
||||||
|
duration: 500,
|
||||||
|
easing: 'easeOutExpo',
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function hide () {
|
||||||
|
const t = (side.value === "right") ? 50 : -80;
|
||||||
|
|
||||||
|
anime({
|
||||||
|
targets: '#textInput',
|
||||||
|
opacity: [1, 0],
|
||||||
|
translateX: t,
|
||||||
|
scale: 0.3,
|
||||||
|
duration: 500,
|
||||||
|
easing: 'easeOutExpo',
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchSide () {
|
||||||
|
const t = (side.value === "right") ? -130 : 110;
|
||||||
|
|
||||||
|
anime({
|
||||||
|
targets: '#textInput',
|
||||||
|
translateX: t,
|
||||||
|
duration: 500,
|
||||||
|
easing: 'easeOutExpo',
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
side,
|
||||||
|
show,
|
||||||
|
hide,
|
||||||
|
switchSide
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export function animateAudioInput () {
|
||||||
|
|
||||||
|
function show () {
|
||||||
|
anime({
|
||||||
|
targets: '#recIcon',
|
||||||
|
opacity: [0, 0.75],
|
||||||
|
scale: [0.0, 1],
|
||||||
|
duration: 250,
|
||||||
|
easing: 'linear',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function hide () {
|
||||||
|
anime({
|
||||||
|
targets: '#recIcon',
|
||||||
|
opacity: [0.75, 0],
|
||||||
|
scale: [1, 0],
|
||||||
|
duration: 250,
|
||||||
|
easing: 'linear',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
show,
|
||||||
|
hide
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function newMessage ({
|
||||||
|
text=false,
|
||||||
|
audio=false,
|
||||||
|
context=false,
|
||||||
|
uid=uuidv4()
|
||||||
|
}) {
|
||||||
|
return {
|
||||||
|
text: text,
|
||||||
|
audio: audio,
|
||||||
|
context: context,
|
||||||
|
uid: uid
|
||||||
|
};
|
||||||
|
}
|
||||||
77
src/render/components/controllers/inputItem.control.audio.ts
Normal file
77
src/render/components/controllers/inputItem.control.audio.ts
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
import { onMounted, onUnmounted, ref, Ref } from "vue";
|
||||||
|
import { postMessage } from "@/render/ipc";
|
||||||
|
import { newMessage, 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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -1,70 +1,20 @@
|
||||||
import anime from "animejs";
|
|
||||||
import useMitt from "@/modules/mitt";
|
|
||||||
import { useIpc } from '@/modules/ipc';
|
|
||||||
import { Ref, ref, watch, onMounted, onUnmounted } from "vue";
|
import { Ref, ref, watch, onMounted, onUnmounted } from "vue";
|
||||||
import keyboardNameMap from "../keyBoardMaps/keyboardNameMap";
|
import { postMessage } from "@/render/ipc";
|
||||||
import { clientMessage, renderMessage } from '@/modules/message';
|
import { newMessage, animateTextInput } from "./helpers";
|
||||||
|
|
||||||
//---Animations-----------------------------------------------
|
|
||||||
|
|
||||||
let side = "right"; // Side of parent we are on.
|
|
||||||
|
|
||||||
function showTextInput () {
|
|
||||||
const t1 = (side === "right") ? 50 : -70;
|
|
||||||
const t2 = (side === "right") ? 110 : -130;
|
|
||||||
|
|
||||||
anime({
|
|
||||||
targets: '#textInput',
|
|
||||||
opacity: [0, 1],
|
|
||||||
translateX: [t1, t2],
|
|
||||||
scale: [0.3, 1],
|
|
||||||
duration: 500,
|
|
||||||
easing: 'easeOutExpo',
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function hideTextInput() {
|
|
||||||
const t = (side === "right") ? 50 : -80;
|
|
||||||
|
|
||||||
anime({
|
|
||||||
targets: '#textInput',
|
|
||||||
opacity: [1, 0],
|
|
||||||
translateX: t,
|
|
||||||
scale: 0.3,
|
|
||||||
duration: 500,
|
|
||||||
easing: 'easeOutExpo',
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function switchSide(currentSide: string) {
|
|
||||||
const t = (currentSide === "right") ? -130 : 110;
|
|
||||||
|
|
||||||
anime({
|
|
||||||
targets: '#textInput',
|
|
||||||
translateX: t,
|
|
||||||
duration: 500,
|
|
||||||
easing: 'easeOutExpo',
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
//------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
export default function useTextInputController(elementX: Ref) {
|
export default function useTextInputController(elementX: Ref) {
|
||||||
|
|
||||||
let textInput: HTMLInputElement | null;
|
let textInput: HTMLInputElement | null;
|
||||||
|
|
||||||
const { post } = useIpc();
|
const { side, show, hide, switchSide } = animateTextInput();
|
||||||
const { emitter } = useMitt();
|
|
||||||
|
|
||||||
let firstKey = true;
|
let firstKey = true;
|
||||||
const typing = ref(false);
|
const typing = ref(false);
|
||||||
|
|
||||||
// Prep inputItem for typing.
|
// Prep inputItem for typing.
|
||||||
const prepInput = () => {
|
const prepInput = () => {
|
||||||
showTextInput()
|
show()
|
||||||
typing.value = true
|
typing.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -76,7 +26,7 @@ export default function useTextInputController(elementX: Ref) {
|
||||||
textInput.blur();
|
textInput.blur();
|
||||||
}
|
}
|
||||||
|
|
||||||
hideTextInput()
|
hide()
|
||||||
firstKey = true;
|
firstKey = true;
|
||||||
typing.value = false;
|
typing.value = false;
|
||||||
}
|
}
|
||||||
|
|
@ -85,28 +35,20 @@ export default function useTextInputController(elementX: Ref) {
|
||||||
const sendMessage = () => {
|
const sendMessage = () => {
|
||||||
if (textInput) {
|
if (textInput) {
|
||||||
|
|
||||||
// Create the message.
|
|
||||||
const message = renderMessage(
|
|
||||||
textInput.value,
|
|
||||||
false,
|
|
||||||
"",
|
|
||||||
"sf"
|
|
||||||
)
|
|
||||||
|
|
||||||
emitter.emit("self-message", message);
|
|
||||||
|
|
||||||
// Send it to the backend for processing.
|
// Send it to the backend for processing.
|
||||||
const clientM = clientMessage(textInput.value, false, message.uid);
|
const message = newMessage({
|
||||||
post('client-message', clientM);
|
text: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// postMessage(message);
|
||||||
|
|
||||||
clearInput()
|
clearInput()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keys that are capable of opening the text input (numbers and letters).
|
// Keys that are capable of opening the text input (numbers and letters).
|
||||||
const hotKeyRange = keyboardNameMap.slice(47, 91)
|
const isHotKey = (key: number) => {
|
||||||
const isHotKey = (key: string) => {
|
if (key >= 47 && key <= 91) { // a letter
|
||||||
if (hotKeyRange.includes(key)) {
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -114,7 +56,7 @@ export default function useTextInputController(elementX: Ref) {
|
||||||
//---Callbacks-----------------------------------------------
|
//---Callbacks-----------------------------------------------
|
||||||
|
|
||||||
const onKeyDown = (e: KeyboardEvent) => {
|
const onKeyDown = (e: KeyboardEvent) => {
|
||||||
const key = keyboardNameMap[e.keyCode]
|
const key = e.keyCode;
|
||||||
|
|
||||||
if (textInput) {
|
if (textInput) {
|
||||||
|
|
||||||
|
|
@ -131,18 +73,18 @@ export default function useTextInputController(elementX: Ref) {
|
||||||
textInput.focus();
|
textInput.focus();
|
||||||
|
|
||||||
// Close input when no text or on ESC.
|
// Close input when no text or on ESC.
|
||||||
if ((textInput.value == "") && (!firstKey) && (key === "BACK_SPACE")) {
|
if ((textInput.value == "") && (!firstKey) && (key === 8)) { // backspace
|
||||||
clearInput()
|
clearInput()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key === "ESCAPE") {
|
if (key === 27) { // escape
|
||||||
clearInput()
|
clearInput()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close and send on enter.
|
// Close and send on enter.
|
||||||
if (key === "ENTER") {
|
if (key === 13) {
|
||||||
if (textInput.value) {
|
if (textInput.value) {
|
||||||
sendMessage()
|
sendMessage()
|
||||||
return
|
return
|
||||||
|
|
@ -160,17 +102,17 @@ export default function useTextInputController(elementX: Ref) {
|
||||||
const winW = window.innerWidth
|
const winW = window.innerWidth
|
||||||
|
|
||||||
// Logic depends on the side we are on.
|
// Logic depends on the side we are on.
|
||||||
if (side === "right") {
|
if (side.value === "right") {
|
||||||
if (winW - elementX < 230) {
|
if (winW - elementX < 230) {
|
||||||
switchSide(side)
|
switchSide()
|
||||||
side = "left"
|
side.value = "left"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else {
|
else {
|
||||||
if (winW - elementX > 230) {
|
if (winW - elementX > 230) {
|
||||||
switchSide(side)
|
switchSide()
|
||||||
side = "right"
|
side.value = "right"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
38
src/render/components/controllers/messenger.control.ts
Normal file
38
src/render/components/controllers/messenger.control.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import useScroll from "@/render/composables/useScroll";
|
||||||
|
|
||||||
|
const messagesRef = ref();
|
||||||
|
|
||||||
|
/* seed the canvas with messages */
|
||||||
|
const seedCanvas = (messages: Message[]) => {
|
||||||
|
messagesRef.value = messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
const addMessage = (message: Message) => {
|
||||||
|
messagesRef.value.push(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateMessage = (message: Message) => {
|
||||||
|
|
||||||
|
let target_message = messagesRef.value.filter((m: Message) => {
|
||||||
|
return m.uid = message.uid;
|
||||||
|
})[0];
|
||||||
|
|
||||||
|
if (target_message) {
|
||||||
|
target_message = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function useMessages() {
|
||||||
|
|
||||||
|
const { updateScrollRef, adjustScroll } = useScroll("messenger");
|
||||||
|
|
||||||
|
return {
|
||||||
|
messagesRef,
|
||||||
|
seedCanvas,
|
||||||
|
addMessage,
|
||||||
|
updateMessage
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
89
src/render/components/header.vue
Normal file
89
src/render/components/header.vue
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
<template>
|
||||||
|
<button id="titlebar" />
|
||||||
|
<span id="menu">
|
||||||
|
<button
|
||||||
|
class="menuButton exitButton"
|
||||||
|
@click.prevent="postNavBarExit"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
class="menuButton minimizeButton"
|
||||||
|
@click.prevent="postNavBarMin"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
|
||||||
|
// import { postNavBarExit, postNavBarMin } from "@/render/ipc";
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: "Header",
|
||||||
|
setup() {
|
||||||
|
const postNavBarExit = () => {};
|
||||||
|
const postNavBarMin = () => {};
|
||||||
|
return {
|
||||||
|
postNavBarExit,
|
||||||
|
postNavBarMin
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
#titlebar {
|
||||||
|
position: fixed;
|
||||||
|
|
||||||
|
width: 100vw;
|
||||||
|
height: 54px;
|
||||||
|
|
||||||
|
opacity: 0.75;
|
||||||
|
|
||||||
|
background-color: #EBEBEB;
|
||||||
|
|
||||||
|
-webkit-app-region: drag;
|
||||||
|
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#menu {
|
||||||
|
position: fixed;
|
||||||
|
margin-left: 20px;
|
||||||
|
margin-top: 20px;
|
||||||
|
|
||||||
|
z-index: 2;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.menuButton {
|
||||||
|
border: none;
|
||||||
|
outline:none;
|
||||||
|
min-width: 14px;
|
||||||
|
min-height: 14px;
|
||||||
|
border-radius: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exitButton {
|
||||||
|
background-color: #FF6157;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exitButton:active {
|
||||||
|
background: #c14645;
|
||||||
|
}
|
||||||
|
|
||||||
|
.minimizeButton {
|
||||||
|
background-color: #FFC12F;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.minimizeButton:active {
|
||||||
|
background-color: #c08e38;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -13,7 +13,10 @@
|
||||||
<span v-if="recording" class="pause"></span>
|
<span v-if="recording" class="pause"></span>
|
||||||
|
|
||||||
<!-- Show text input on key-down -->
|
<!-- Show text input on key-down -->
|
||||||
<TextInput />
|
<input
|
||||||
|
id="textInput"
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- Show suggestions menu on click -->
|
<!-- Show suggestions menu on click -->
|
||||||
<!-- <Menu /> -->
|
<!-- <Menu /> -->
|
||||||
|
|
@ -24,24 +27,19 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
|
||||||
import { defineComponent } from "vue";
|
import { defineComponent } from "vue";
|
||||||
import draggify from "@/modules/draggify";
|
import draggify from "@/render/composables/useDraggify";
|
||||||
|
|
||||||
import TextInput from "@/components/textInput.vue";
|
|
||||||
|
|
||||||
import useTextInputController from
|
import useTextInputController from
|
||||||
"@/components/controllers/textCtrl";
|
"@/render/components/controllers/inputItem.control.text";
|
||||||
import useAudioInputController from
|
import useAudioInputController from
|
||||||
"@/components/controllers/audioCtrl";
|
"@/render/components/controllers/inputItem.control.audio";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: "InputItem",
|
name: "InputItem",
|
||||||
|
|
||||||
props: ["initials"],
|
props: ["initials"],
|
||||||
|
|
||||||
components: {
|
|
||||||
TextInput
|
|
||||||
},
|
|
||||||
|
|
||||||
setup() {
|
setup() {
|
||||||
|
|
||||||
// Default values for position.
|
// Default values for position.
|
||||||
|
|
@ -191,4 +189,32 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#textInput {
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
opacity: 0;
|
||||||
|
|
||||||
|
min-width: 150px;
|
||||||
|
height: 16px;
|
||||||
|
|
||||||
|
border-radius: 18px;
|
||||||
|
|
||||||
|
padding: 10px;
|
||||||
|
margin-right: 10px;
|
||||||
|
margin-left: 10px;
|
||||||
|
|
||||||
|
outline: none;
|
||||||
|
border: none;
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
background-color: white;
|
||||||
|
|
||||||
|
z-index: -1;
|
||||||
|
|
||||||
|
box-shadow: 0px 0px 10px 0px rgba(0,0,0,0.15);
|
||||||
|
|
||||||
|
transform: translateX(50px) scale(0.3);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
@ -35,23 +35,34 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
|
||||||
import { defineComponent, ref } from "vue";
|
import { defineComponent, ref } from "vue";
|
||||||
import { useIpc } from "@/modules/ipc";
|
import { setProfile } from '@/render/composables/useProfile';
|
||||||
import { authRequest } from '@/modules/message';
|
import { invokeLogin } from "@/render/ipc";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: "Login",
|
name: "Login",
|
||||||
|
|
||||||
setup() {
|
setup() {
|
||||||
|
|
||||||
const { post } = useIpc();
|
|
||||||
|
|
||||||
const usr = ref("");
|
const usr = ref("");
|
||||||
const pwd = ref("");
|
const pwd = ref("");
|
||||||
|
|
||||||
// Submit login credentials to the backend.
|
// Submit login credentials to the backend.
|
||||||
const submitForm = () => {
|
const submitForm = async () => {
|
||||||
console.log(`Submitting login form: ${usr.value}, ${pwd.value}`)
|
|
||||||
post("client-message", authRequest(false, usr.value, pwd.value))
|
try {
|
||||||
|
|
||||||
|
const profile = await invokeLogin({
|
||||||
|
email: usr.value,
|
||||||
|
password: pwd.value
|
||||||
|
}) as Profile;
|
||||||
|
|
||||||
|
setProfile(profile)
|
||||||
|
|
||||||
|
|
||||||
|
} catch(e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
82
src/render/components/messenger.vue
Normal file
82
src/render/components/messenger.vue
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
<template>
|
||||||
|
|
||||||
|
<!-- Position fixed items -->
|
||||||
|
<InputItem :initials="profile.initials"/>
|
||||||
|
<Settings />
|
||||||
|
<div id="recIcon" />
|
||||||
|
|
||||||
|
<!-- List of message bubbles. -->
|
||||||
|
<div id="messenger">
|
||||||
|
<Bubble
|
||||||
|
v-for="message in messages"
|
||||||
|
:text="message.content.text"
|
||||||
|
:context="message.context"
|
||||||
|
:key="message[0]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
|
||||||
|
import { defineComponent, onMounted, onUnmounted } from "vue";
|
||||||
|
import InputItem from "@/render/components/inputItem.vue";
|
||||||
|
import Settings from "@/render/components/settings.vue";
|
||||||
|
import Bubble from "@/render/components/bubble.vue";
|
||||||
|
|
||||||
|
import { profile } from "@/render/composables/useProfile";
|
||||||
|
import { messages } from "@/render/composables/useMessages";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: "Messenger",
|
||||||
|
|
||||||
|
components: {
|
||||||
|
InputItem,
|
||||||
|
Settings,
|
||||||
|
Bubble
|
||||||
|
},
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
return {
|
||||||
|
messages,
|
||||||
|
profile
|
||||||
|
};
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
|
#messenger {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#messenger::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#recIcon {
|
||||||
|
position: fixed;
|
||||||
|
right: 0;
|
||||||
|
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.0);
|
||||||
|
|
||||||
|
margin-top: 24px;
|
||||||
|
margin-right: 66px;
|
||||||
|
|
||||||
|
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #FF3B3B;
|
||||||
|
|
||||||
|
z-index: 2;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
@ -28,8 +28,8 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
|
||||||
import { defineComponent, ref } from "vue";
|
import { defineComponent, ref } from "vue";
|
||||||
import { useIpc } from "@/modules/ipc";
|
import { clearProfile } from "@/render/composables/useProfile"
|
||||||
import { logoutRequest } from '@/modules/message';
|
import { invokeLogout } from "@/render/ipc";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: "Settings",
|
name: "Settings",
|
||||||
|
|
@ -37,8 +37,6 @@
|
||||||
setup() {
|
setup() {
|
||||||
const toggleSettings = ref(false);
|
const toggleSettings = ref(false);
|
||||||
|
|
||||||
const { post } = useIpc();
|
|
||||||
|
|
||||||
// Listen for escape key to close settings.
|
// Listen for escape key to close settings.
|
||||||
const onEscape = (e: any) => {
|
const onEscape = (e: any) => {
|
||||||
if(e.key === "Escape") {
|
if(e.key === "Escape") {
|
||||||
|
|
@ -54,9 +52,17 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// We ask server to log us out.
|
// We ask server to log us out.
|
||||||
const onLogout = () => {
|
const onLogout = async () => {
|
||||||
console.log("Submitting logout request.")
|
|
||||||
post("client-message", logoutRequest())
|
console.log("Submitting logout request.");
|
||||||
|
|
||||||
|
try {
|
||||||
|
await invokeLogout();
|
||||||
|
clearProfile();
|
||||||
|
} catch(e) {
|
||||||
|
console.log('error')
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
54
src/render/composables/useIpcRend.ts
Normal file
54
src/render/composables/useIpcRend.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
|
||||||
|
import { IpcRendererEvent } from "electron";
|
||||||
|
|
||||||
|
export class IpcRendererListener<InputParam> implements IIpcListener<InputParam> {
|
||||||
|
|
||||||
|
readonly channel: string;
|
||||||
|
|
||||||
|
readonly _listenerCallback: IpcListenerCallback<InputParam>;
|
||||||
|
|
||||||
|
constructor(options: {
|
||||||
|
channel: string;
|
||||||
|
listenerCallback: IpcListenerCallback<InputParam>;
|
||||||
|
}) {
|
||||||
|
this.channel = options.channel;
|
||||||
|
this._listenerCallback = options.listenerCallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
listen() {
|
||||||
|
this.remove();
|
||||||
|
window.ipcRenderer.on(this.channel, this._onPost);
|
||||||
|
}
|
||||||
|
|
||||||
|
remove() {
|
||||||
|
window.ipcRenderer.removeAllListeners(this.channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _onPost = (_e: IpcRendererEvent, payload: InputParam): void => {
|
||||||
|
console.log(`[IPC] Post: ${this.channel}`);
|
||||||
|
this._listenerCallback(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default function useIpcRenderer () {
|
||||||
|
|
||||||
|
const invoke = async (endpoint: string, payload: any) => {
|
||||||
|
try {
|
||||||
|
const res = await window.ipcRenderer.invoke(endpoint, payload);
|
||||||
|
return res;
|
||||||
|
} catch (e) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const post = (endpoint: string, payload: any) => {
|
||||||
|
window.ipcRenderer.send(endpoint, payload);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
invoke,
|
||||||
|
post,
|
||||||
|
}
|
||||||
|
}
|
||||||
33
src/render/composables/useMessages.ts
Normal file
33
src/render/composables/useMessages.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
// shared
|
||||||
|
import { ref, Ref } from "vue";
|
||||||
|
import useScroll from "@/render/composables/useScroll";
|
||||||
|
|
||||||
|
export const messages: Ref<Array<Message>> = ref([]);
|
||||||
|
|
||||||
|
export const setMessages: IpcListenerCallback<Array<Message>> = (payload) => {
|
||||||
|
messages.value = payload as Array<Message>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const addMessage: IpcListenerCallback<Message> = (payload) => {
|
||||||
|
messages.value.push(payload as Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateMessage: IpcListenerCallback<Message> = (payload) => {
|
||||||
|
const message = payload as Message;
|
||||||
|
|
||||||
|
let targetMessage = messages.value.filter((m: Message) => {
|
||||||
|
return m.uid = message.uid;
|
||||||
|
})[0];
|
||||||
|
|
||||||
|
if (targetMessage) {
|
||||||
|
targetMessage = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
messages,
|
||||||
|
setMessages,
|
||||||
|
addMessage,
|
||||||
|
updateMessage
|
||||||
|
};
|
||||||
27
src/render/composables/useProfile.ts
Normal file
27
src/render/composables/useProfile.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
// shared
|
||||||
|
import { ref } from "vue";
|
||||||
|
|
||||||
|
export const profile = ref();
|
||||||
|
|
||||||
|
export const authComplete = ref(false);
|
||||||
|
|
||||||
|
export const setProfile: IpcListenerCallback<Profile | null> = (payload) => {
|
||||||
|
payload ? profile.value = payload : clearProfile();
|
||||||
|
showRender();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const clearProfile = () => {
|
||||||
|
profile.value = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const showRender = () => {
|
||||||
|
authComplete.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
setProfile,
|
||||||
|
clearProfile,
|
||||||
|
profile,
|
||||||
|
showRender,
|
||||||
|
authComplete,
|
||||||
|
};
|
||||||
29
src/render/composables/useScroll.ts
Normal file
29
src/render/composables/useScroll.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
|
||||||
|
|
||||||
|
export default function useScroll(element: string) {
|
||||||
|
|
||||||
|
let isScrolledToBottom: boolean;
|
||||||
|
const view = document.getElementById(element)
|
||||||
|
|
||||||
|
// Update isScrolledToBottom
|
||||||
|
const updateScrollRef = () => {
|
||||||
|
if (view) isScrolledToBottom = view.scrollHeight - view.clientHeight <= view.scrollTop + 1;
|
||||||
|
return isScrolledToBottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjust scroll after we add content to the messenger.
|
||||||
|
const adjustScroll = () => {
|
||||||
|
if (view) {
|
||||||
|
view.scrollTo({
|
||||||
|
top: view.scrollHeight - view.clientHeight,
|
||||||
|
behavior: 'smooth'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
updateScrollRef,
|
||||||
|
adjustScroll,
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
80
src/render/ipc.ts
Normal file
80
src/render/ipc.ts
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
|
||||||
|
import useIpc from "@/render/composables/useIpcRend";
|
||||||
|
import * as rendererListeners from "./listeners";
|
||||||
|
|
||||||
|
const { post, invoke } = useIpc();
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Account and auth related endpoints
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
export const invokeLogin = async (
|
||||||
|
payload: LoginPayload
|
||||||
|
): Promise<Profile | Error> => (
|
||||||
|
await invoke('invoke-account-login', JSON.stringify(payload))
|
||||||
|
);
|
||||||
|
|
||||||
|
export const invokeLogout = async (): Promise<void> => (
|
||||||
|
await invoke("invoke-account-logout", null)
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Audio endpoints
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const postAudioChunk = (chunk: ArrayBuffer): void => (
|
||||||
|
post("post-audio-collect", chunk)
|
||||||
|
);
|
||||||
|
|
||||||
|
export const invokeReturnAudio = async (): Promise<ArrayBuffer[] | Error> => (
|
||||||
|
await invoke("invoke-audio-flush", null)
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Crimata Platform (session) endpoints
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const invokeSession = async (cid: string): Promise<Profile | Error> => (
|
||||||
|
await invoke("messenger-init", cid)
|
||||||
|
);
|
||||||
|
|
||||||
|
export const postMessage = (payload: Message): void => (
|
||||||
|
post('post-session-send', payload)
|
||||||
|
);
|
||||||
|
|
||||||
|
export const postAppMount = (): void => (
|
||||||
|
post('post-app-mount', null)
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Ipc Renderer Listeners
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
let ipcListeners: IPCListeners = {};
|
||||||
|
|
||||||
|
export const initIpcRendererListeners = () => {
|
||||||
|
for (const [key, listener] of Object.entries(rendererListeners)) {
|
||||||
|
if (!(key in ipcListeners)) {
|
||||||
|
ipcListeners[key] = listener;
|
||||||
|
listener.listen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const removeListeners = () => {
|
||||||
|
for (const [key, listener] of Object.entries(rendererListeners)) {
|
||||||
|
listener.remove();
|
||||||
|
}
|
||||||
|
ipcListeners = {};
|
||||||
|
};
|
||||||
|
|
||||||
32
src/render/listeners.ts
Normal file
32
src/render/listeners.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
|
||||||
|
import { IpcRendererListener } from "./composables/useIpcRend"
|
||||||
|
import { setProfile } from "./composables/useProfile";
|
||||||
|
import { setMessages, addMessage, updateMessage } from "./composables/useMessages";
|
||||||
|
|
||||||
|
const SET_PROFILE_CHANNEL = "set-profile";
|
||||||
|
|
||||||
|
const INIT_MESSAGES_CHANNEL = "init-messages";
|
||||||
|
const ADD_MESSAGE_CHANNEL = "add-message";
|
||||||
|
const UPDATE_MESSAGE_CHANNEL = "update-message";
|
||||||
|
|
||||||
|
export const setProfileListener = new IpcRendererListener({
|
||||||
|
channel: SET_PROFILE_CHANNEL,
|
||||||
|
listenerCallback: setProfile
|
||||||
|
});
|
||||||
|
|
||||||
|
export const initMessagesListener = new IpcRendererListener({
|
||||||
|
channel: INIT_MESSAGES_CHANNEL,
|
||||||
|
listenerCallback: setMessages
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
export const addMessagesListener = new IpcRendererListener({
|
||||||
|
channel: ADD_MESSAGE_CHANNEL,
|
||||||
|
listenerCallback: addMessage
|
||||||
|
});
|
||||||
|
|
||||||
|
export const updateMessagesListener = new IpcRendererListener({
|
||||||
|
channel: UPDATE_MESSAGE_CHANNEL,
|
||||||
|
listenerCallback: updateMessage
|
||||||
|
});
|
||||||
|
|
||||||
21
src/render/main.ts
Normal file
21
src/render/main.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
|
||||||
|
// src/main.ts
|
||||||
|
|
||||||
|
import App from "./App.vue";
|
||||||
|
|
||||||
|
import mitt from "mitt";
|
||||||
|
import { createApp } from "vue";
|
||||||
|
|
||||||
|
import { initIpcRendererListeners } from "./ipc"
|
||||||
|
|
||||||
|
|
||||||
|
// Handle ipcMain events.
|
||||||
|
initIpcRendererListeners();
|
||||||
|
|
||||||
|
// Handle events.
|
||||||
|
const emitter = mitt();
|
||||||
|
|
||||||
|
const app = createApp(App);
|
||||||
|
|
||||||
|
app.provide("mitt", emitter);
|
||||||
|
app.mount("#app");
|
||||||
101
src/session.ts
Normal file
101
src/session.ts
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// import useAudio from "@/audio";
|
||||||
|
import { ipcEmit } from "@/composables/useEmitter";
|
||||||
|
import useWebsockets from "./composables/useWebsockets";
|
||||||
|
import {config} from "@/config";
|
||||||
|
|
||||||
|
/* data structure of messages that's tied to the UI */
|
||||||
|
const uiState: any | null = null;
|
||||||
|
|
||||||
|
/* start and stop audio functionality */
|
||||||
|
// const { initAudio, closeAudio } = useAudio();
|
||||||
|
|
||||||
|
const isInitMessage = (message: any): boolean => {
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const deauthenticate = (): void => {
|
||||||
|
console.log('deauthenticating')
|
||||||
|
};
|
||||||
|
|
||||||
|
const isAddMessage = (message: any): boolean => {
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controls for interfacing with the platform.
|
||||||
|
* Takes an onMessage callback which we define below.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
const onMessageCallback = (payload: string) => {
|
||||||
|
|
||||||
|
const message = JSON.parse(payload);
|
||||||
|
console.log(typeof message);
|
||||||
|
|
||||||
|
/* if the platform fails to authenticate, we must back down */
|
||||||
|
if (message === "CLOSE_AUTH_FAIL") {
|
||||||
|
deauthenticate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ipcEmit("add-message", message)
|
||||||
|
return
|
||||||
|
|
||||||
|
/* on init, platform sends state, used to init canvas */
|
||||||
|
if (isInitMessage(message)) {
|
||||||
|
ipcEmit("init-messages", message)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
else if (isAddMessage(message)) {
|
||||||
|
ipcEmit("add-messages", message)
|
||||||
|
}
|
||||||
|
|
||||||
|
else {
|
||||||
|
ipcEmit("update-messages", message)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const onConnectionStatusCallback = (alive: boolean) => {
|
||||||
|
// console.log('[Session]: Connection Alive: ', alive);
|
||||||
|
// ipcEmit('connection-state', alive);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { connect, send, close } = useWebsockets(
|
||||||
|
onMessageCallback,
|
||||||
|
onConnectionStatusCallback
|
||||||
|
);
|
||||||
|
|
||||||
|
/* send a message to the platform */
|
||||||
|
export function sendMessage<Message>(message: Message): void {
|
||||||
|
|
||||||
|
/* socket send */
|
||||||
|
send(message);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* launch a new session (the main process for authenticated users) */
|
||||||
|
export function launchSession(platformKey: string) {
|
||||||
|
|
||||||
|
/* connect to the platform */
|
||||||
|
connect(config.PLATFORM_URL, platformKey);
|
||||||
|
|
||||||
|
/* initialize the audio streams */
|
||||||
|
// initAudio();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function endSession() {
|
||||||
|
|
||||||
|
// closeAudio();
|
||||||
|
|
||||||
|
close();
|
||||||
|
|
||||||
|
}
|
||||||
31
src/store.ts
Normal file
31
src/store.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
const Store = require('electron-store');
|
||||||
|
|
||||||
|
const schema = {
|
||||||
|
token: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
profile: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
const store = new Store({
|
||||||
|
schema,
|
||||||
|
encryptionKey: "super user test"
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getToken = (): string | undefined => (store.get("token"));
|
||||||
|
|
||||||
|
export const clearToken = (): void => (store.delete("token"));
|
||||||
|
|
||||||
|
export const setToken = (token: string): void => (store.set('token', token));
|
||||||
|
|
||||||
|
export const setProfile = (profile: Profile): Profile => (store.set('profile', profile));
|
||||||
|
|
||||||
|
export const getProfile = (): Profile => (store.get('profile'));
|
||||||
|
|
||||||
|
export const clearProfile = (): void => (store.delete('profile'));
|
||||||
|
|
||||||
|
export const clearStore = (): void => {
|
||||||
|
clearToken();
|
||||||
|
clearProfile();
|
||||||
|
}
|
||||||
|
|
||||||
101
src/types.ts
101
src/types.ts
|
|
@ -1,74 +1,77 @@
|
||||||
export interface RenderMessage {
|
|
||||||
content: {
|
interface Message {
|
||||||
text: boolean | string;
|
text: boolean | string;
|
||||||
|
context: boolean | string;
|
||||||
audio: boolean | string;
|
audio: boolean | string;
|
||||||
};
|
type: 1 | 2 | 3;
|
||||||
context: string;
|
|
||||||
modifier: string;
|
|
||||||
time: number;
|
|
||||||
uid: string;
|
|
||||||
isChild: string;
|
|
||||||
seen: boolean;
|
|
||||||
newMessage: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ClientMessage {
|
|
||||||
text: string;
|
|
||||||
audio: boolean | string;
|
|
||||||
uid: string;
|
uid: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ClientRequest {
|
interface ViewMessage extends Message {
|
||||||
intent: string;
|
child: string;
|
||||||
params: object;
|
|
||||||
epic: string | boolean;
|
|
||||||
confidence: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SessionState {
|
interface WindowState {
|
||||||
key: string | boolean;
|
|
||||||
newMessages: RenderMessage[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WindowState {
|
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
x: number | null;
|
x: number | null;
|
||||||
y: number | null;
|
y: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AuthRequest {
|
interface Profile {
|
||||||
key: boolean | string;
|
|
||||||
usr: boolean | string;
|
|
||||||
pwd: boolean | string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Profile {
|
|
||||||
crimataId: string;
|
crimataId: string;
|
||||||
alias: string;
|
alias: string;
|
||||||
initials: string;
|
initials: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AuthProtocol {
|
interface LoginPayload {
|
||||||
key: boolean | string;
|
email: string;
|
||||||
profile: boolean | Profile;
|
password: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LogoutRequest {
|
interface AuthState {
|
||||||
logout: boolean;
|
profile: Profile | null;
|
||||||
|
token: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Annotation {
|
interface AccountCredentials {
|
||||||
text: string;
|
email: string;
|
||||||
context: string;
|
password: string;
|
||||||
uid: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StandardMessage {
|
/*
|
||||||
content: {
|
* Electron Ipc
|
||||||
text: boolean | string;
|
*/
|
||||||
audio: boolean | string;
|
interface IpcHandlerCallback<I, O> {
|
||||||
};
|
(payload: I | null): Promise<Error | O>;
|
||||||
context: string;
|
|
||||||
modifier: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface IpcListenerCallback<T> {
|
||||||
|
(payload: T | null): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IIpcHandler<I, O> {
|
||||||
|
handle(): void;
|
||||||
|
remove(): void;
|
||||||
|
readonly _handlerCallback: IpcHandlerCallback<I, O>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IIpcListener<I> {
|
||||||
|
listen(): void;
|
||||||
|
remove(): void;
|
||||||
|
readonly _listenerCallback: IpcListenerCallback<I>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IPCHandlers {
|
||||||
|
[handler: string]: IIpcHandler<any, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IPCListeners {
|
||||||
|
[listener: string]: IIpcListener<any> | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IpcRendererEvent<T> {
|
||||||
|
channel: string;
|
||||||
|
payload: T | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,37 @@
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
import { BrowserWindow, ipcMain } from "electron";
|
import { BrowserWindow, ipcMain, app } from "electron";
|
||||||
import { createProtocol } from "vue-cli-plugin-electron-builder/lib";
|
import { createProtocol } from "vue-cli-plugin-electron-builder/lib";
|
||||||
import { backgroundMitt } from '@/modules/emitter';
|
import { backgroundMitt } from './composables/useEmitter';
|
||||||
import { RenderMessage, WindowState } from "@/types";
|
import { saveToJson } from "./composables/useSaveToJSON";
|
||||||
import { loadWinState, saveToJson } from "./helpers";
|
|
||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
|
import fs from 'fs';
|
||||||
|
import { config } from "@/config";
|
||||||
const { autoUpdater } = require('electron-updater');
|
const { autoUpdater } = require('electron-updater');
|
||||||
|
|
||||||
interface IpcRendererPayload {
|
|
||||||
endpoint: string;
|
|
||||||
message: RenderMessage | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
let win: BrowserWindow | null;
|
let win: BrowserWindow | null;
|
||||||
let winState: WindowState;
|
let winState: WindowState;
|
||||||
|
|
||||||
|
const loadWinState = (fileName: string): WindowState => {
|
||||||
|
let state: WindowState;
|
||||||
|
|
||||||
|
try {
|
||||||
|
state = JSON.parse(fs.readFileSync(config.configPath + fileName).toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
catch (error) {
|
||||||
|
state = {
|
||||||
|
width: 600,
|
||||||
|
height: 500,
|
||||||
|
x: null,
|
||||||
|
y: null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return state
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
// Called when a NavBar button is pressed.
|
// Called when a NavBar button is pressed.
|
||||||
const onNavBar = (_event: any, action: string): void => {
|
const onNavBar = (_event: any, action: string): void => {
|
||||||
if (win) {
|
if (win) {
|
||||||
|
|
@ -28,11 +44,9 @@ const onNavBar = (_event: any, action: string): void => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Util function to render message on ipc-renderer event.
|
// Util function to render message on ipc-renderer event.
|
||||||
const renderMessage = (payload: IpcRendererPayload): void => {
|
const postToWindow = <T>(event: IpcRendererEvent<T>): void => {
|
||||||
if (win) {
|
if (win) {
|
||||||
win.webContents.send(payload.endpoint, {
|
win.webContents.send(event.channel, event.payload);
|
||||||
message: payload.message
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -57,7 +71,6 @@ const saveWindowState = () => {
|
||||||
|
|
||||||
// Do this on window mount.
|
// Do this on window mount.
|
||||||
const onWindowMount = (): void => {
|
const onWindowMount = (): void => {
|
||||||
console.log("BW:Adding window listeners. ")
|
|
||||||
|
|
||||||
// Must tell initApp that window exists.
|
// Must tell initApp that window exists.
|
||||||
backgroundMitt.emit('window-active', true);
|
backgroundMitt.emit('window-active', true);
|
||||||
|
|
@ -68,20 +81,18 @@ const onWindowMount = (): void => {
|
||||||
|
|
||||||
// Gateway for messages to the frontend.
|
// Gateway for messages to the frontend.
|
||||||
backgroundMitt.removeAllListeners("ipc-renderer")
|
backgroundMitt.removeAllListeners("ipc-renderer")
|
||||||
backgroundMitt.on("ipc-renderer", renderMessage);
|
backgroundMitt.on("ipc-renderer", postToWindow);
|
||||||
|
|
||||||
console.log("BW:Listeners created.")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do this on window dismount (close).
|
// Do this on window dismount (close).
|
||||||
const onWindowDismount = (): void => {
|
const onWindowDismount = (): void => {
|
||||||
console.log("BW:Window closed.")
|
|
||||||
win = null;
|
win = null;
|
||||||
backgroundMitt.emit('window-active', false);
|
backgroundMitt.emit('window-active', false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// function used by run.ts to create the main window.
|
// function used by run.ts to create the main window.
|
||||||
export async function createWindow(): Promise<void> {
|
export default async function createWindow(): Promise<void> {
|
||||||
return new Promise((resolve, _reject) => {
|
return new Promise((resolve, _reject) => {
|
||||||
|
|
||||||
// avoid creating duplicate windows.
|
// avoid creating duplicate windows.
|
||||||
|
|
@ -89,14 +100,13 @@ export async function createWindow(): Promise<void> {
|
||||||
|
|
||||||
// Load the saved window state.
|
// Load the saved window state.
|
||||||
winState = loadWinState("window.json")
|
winState = loadWinState("window.json")
|
||||||
console.log(`BW:Creating window [${winState.width}, ${winState.height}].`)
|
|
||||||
|
|
||||||
// Define the browser window.
|
// Define the browser window.
|
||||||
win = new BrowserWindow({
|
win = new BrowserWindow({
|
||||||
width: winState.width,
|
width: winState.width,
|
||||||
height: winState.height,
|
height: winState.height,
|
||||||
x: winState.x,
|
x: winState.x as number,
|
||||||
y: winState.y,
|
y: winState.y as number,
|
||||||
resizable: true,
|
resizable: true,
|
||||||
backgroundColor: '#EBEBEB',
|
backgroundColor: '#EBEBEB',
|
||||||
frame: false,
|
frame: false,
|
||||||
|
|
@ -6,6 +6,7 @@ const wss = new WebSocket.Server({
|
||||||
let auth = false;
|
let auth = false;
|
||||||
|
|
||||||
wss.on("connection", function connection(ws, req) {
|
wss.on("connection", function connection(ws, req) {
|
||||||
|
|
||||||
ws.on("message", function incoming(message) {
|
ws.on("message", function incoming(message) {
|
||||||
|
|
||||||
console.log(message)
|
console.log(message)
|
||||||
|
|
@ -19,13 +20,13 @@ wss.on("connection", function connection(ws, req) {
|
||||||
auth = true;
|
auth = true;
|
||||||
} else {
|
} else {
|
||||||
const parsed = JSON.parse(message);
|
const parsed = JSON.parse(message);
|
||||||
console.log('got a message', message)
|
console.log('got a message', message);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
const parsed = JSON.parse(message);
|
const parsed = JSON.parse(message);
|
||||||
console.log('parsed', parsed)
|
console.log('parsed', parsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
|
"**/*.ts",
|
||||||
"src/*.ts",
|
"src/*.ts",
|
||||||
"src/**/*.ts",
|
"src/**/*.ts",
|
||||||
"src/**/*.tsx",
|
"src/**/*.tsx",
|
||||||
|
|
|
||||||
123
yarn.lock
123
yarn.lock
|
|
@ -1279,6 +1279,13 @@
|
||||||
resolved "https://registry.yarnpkg.com/@types/anymatch/-/anymatch-1.3.1.tgz#336badc1beecb9dacc38bea2cf32adf627a8421a"
|
resolved "https://registry.yarnpkg.com/@types/anymatch/-/anymatch-1.3.1.tgz#336badc1beecb9dacc38bea2cf32adf627a8421a"
|
||||||
integrity sha512-/+CRPXpBDpo2RK9C68N3b2cOvO0Cf5B9aPijHsoDQTHivnGSObdOF2BRQOYjojWTDy6nQvMjmqRXIxH55VjxxA==
|
integrity sha512-/+CRPXpBDpo2RK9C68N3b2cOvO0Cf5B9aPijHsoDQTHivnGSObdOF2BRQOYjojWTDy6nQvMjmqRXIxH55VjxxA==
|
||||||
|
|
||||||
|
"@types/axios@^0.14.0":
|
||||||
|
version "0.14.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/axios/-/axios-0.14.0.tgz#ec2300fbe7d7dddd7eb9d3abf87999964cafce46"
|
||||||
|
integrity sha1-7CMA++fX3d1+udOr+HmZlkyvzkY=
|
||||||
|
dependencies:
|
||||||
|
axios "*"
|
||||||
|
|
||||||
"@types/babel__core@^7.1.0":
|
"@types/babel__core@^7.1.0":
|
||||||
version "7.1.12"
|
version "7.1.12"
|
||||||
resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.12.tgz#4d8e9e51eb265552a7e4f1ff2219ab6133bdfb2d"
|
resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.12.tgz#4d8e9e51eb265552a7e4f1ff2219ab6133bdfb2d"
|
||||||
|
|
@ -2348,6 +2355,13 @@ ajv-errors@^1.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/ajv-errors/-/ajv-errors-1.0.1.tgz#f35986aceb91afadec4102fbd85014950cefa64d"
|
resolved "https://registry.yarnpkg.com/ajv-errors/-/ajv-errors-1.0.1.tgz#f35986aceb91afadec4102fbd85014950cefa64d"
|
||||||
integrity sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==
|
integrity sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==
|
||||||
|
|
||||||
|
ajv-formats@^2.0.2:
|
||||||
|
version "2.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.0.tgz#96eaf83e38d32108b66d82a9cb0cfa24886cdfeb"
|
||||||
|
integrity sha512-USH2jBb+C/hIpwD2iRjp0pe0k+MvzG0mlSn/FIdCgQhUb9ALPRjt2KIQdfZDS9r0ZIeUAg7gOu9KL0PFqGqr5Q==
|
||||||
|
dependencies:
|
||||||
|
ajv "^8.0.0"
|
||||||
|
|
||||||
ajv-keywords@^3.1.0, ajv-keywords@^3.4.1, ajv-keywords@^3.5.2:
|
ajv-keywords@^3.1.0, ajv-keywords@^3.4.1, ajv-keywords@^3.5.2:
|
||||||
version "3.5.2"
|
version "3.5.2"
|
||||||
resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d"
|
resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d"
|
||||||
|
|
@ -2363,6 +2377,16 @@ ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.0, ajv@^6.12.2, ajv@^6.12.3, ajv
|
||||||
json-schema-traverse "^0.4.1"
|
json-schema-traverse "^0.4.1"
|
||||||
uri-js "^4.2.2"
|
uri-js "^4.2.2"
|
||||||
|
|
||||||
|
ajv@^8.0.0, ajv@^8.1.0:
|
||||||
|
version "8.5.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.5.0.tgz#695528274bcb5afc865446aa275484049a18ae4b"
|
||||||
|
integrity sha512-Y2l399Tt1AguU3BPRP9Fn4eN+Or+StUGWCUpbnFyXSo8NZ9S4uj+AG2pjs5apK+ZMOwYOz1+a+VKvKH7CudXgQ==
|
||||||
|
dependencies:
|
||||||
|
fast-deep-equal "^3.1.1"
|
||||||
|
json-schema-traverse "^1.0.0"
|
||||||
|
require-from-string "^2.0.2"
|
||||||
|
uri-js "^4.2.2"
|
||||||
|
|
||||||
alphanum-sort@^1.0.0:
|
alphanum-sort@^1.0.0:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3"
|
resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3"
|
||||||
|
|
@ -2710,6 +2734,11 @@ atob@^2.1.2:
|
||||||
resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
|
resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
|
||||||
integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==
|
integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==
|
||||||
|
|
||||||
|
atomically@^1.7.0:
|
||||||
|
version "1.7.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/atomically/-/atomically-1.7.0.tgz#c07a0458432ea6dbc9a3506fffa424b48bccaafe"
|
||||||
|
integrity sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w==
|
||||||
|
|
||||||
autoprefixer@^9.8.6:
|
autoprefixer@^9.8.6:
|
||||||
version "9.8.6"
|
version "9.8.6"
|
||||||
resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.8.6.tgz#3b73594ca1bf9266320c5acf1588d74dea74210f"
|
resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.8.6.tgz#3b73594ca1bf9266320c5acf1588d74dea74210f"
|
||||||
|
|
@ -2733,6 +2762,13 @@ aws4@^1.8.0:
|
||||||
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59"
|
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59"
|
||||||
integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==
|
integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==
|
||||||
|
|
||||||
|
axios@*, axios@^0.21.1:
|
||||||
|
version "0.21.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.1.tgz#22563481962f4d6bde9a76d516ef0e5d3c09b2b8"
|
||||||
|
integrity sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==
|
||||||
|
dependencies:
|
||||||
|
follow-redirects "^1.10.0"
|
||||||
|
|
||||||
babel-code-frame@^6.22.0, babel-code-frame@^6.26.0:
|
babel-code-frame@^6.22.0, babel-code-frame@^6.26.0:
|
||||||
version "6.26.0"
|
version "6.26.0"
|
||||||
resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b"
|
resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b"
|
||||||
|
|
@ -3946,6 +3982,22 @@ condense-newlines@^0.2.1:
|
||||||
is-whitespace "^0.3.0"
|
is-whitespace "^0.3.0"
|
||||||
kind-of "^3.0.2"
|
kind-of "^3.0.2"
|
||||||
|
|
||||||
|
conf@^10.0.0:
|
||||||
|
version "10.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/conf/-/conf-10.0.1.tgz#038093e5cbddc0e59bc14f63382c4ce732a4781d"
|
||||||
|
integrity sha512-QClEoNcruwBL84QgMEPHibL3ERxWIrRKhbjJKG1VsFBadm5QpS0jsu4QjY/maxUvhyAKXeyrs+ws+lC6PajnEg==
|
||||||
|
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, config-chain@^1.1.12:
|
config-chain@^1.1.11, config-chain@^1.1.12:
|
||||||
version "1.1.12"
|
version "1.1.12"
|
||||||
resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.12.tgz#0fde8d091200eb5e808caf25fe618c02f48e4efa"
|
resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.12.tgz#0fde8d091200eb5e808caf25fe618c02f48e4efa"
|
||||||
|
|
@ -4468,6 +4520,13 @@ deasync@^0.1.15:
|
||||||
bindings "^1.5.0"
|
bindings "^1.5.0"
|
||||||
node-addon-api "^1.7.1"
|
node-addon-api "^1.7.1"
|
||||||
|
|
||||||
|
debounce-fn@^4.0.0:
|
||||||
|
version "4.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/debounce-fn/-/debounce-fn-4.0.0.tgz#ed76d206d8a50e60de0dd66d494d82835ffe61c7"
|
||||||
|
integrity sha512-8pYCQiL9Xdcg0UPSD3d+0KMlOjp+KGU5EPwYddgzQ7DATsg4fuUDjQtsYLmWjnk2obnNHgV3vE2Y4jejSOJVBQ==
|
||||||
|
dependencies:
|
||||||
|
mimic-fn "^3.0.0"
|
||||||
|
|
||||||
debug@2.6.9, 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"
|
version "2.6.9"
|
||||||
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
|
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
|
||||||
|
|
@ -4827,11 +4886,23 @@ dot-prop@^5.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
is-obj "^2.0.0"
|
is-obj "^2.0.0"
|
||||||
|
|
||||||
|
dot-prop@^6.0.1:
|
||||||
|
version "6.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-6.0.1.tgz#fc26b3cf142b9e59b74dbd39ed66ce620c681083"
|
||||||
|
integrity sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==
|
||||||
|
dependencies:
|
||||||
|
is-obj "^2.0.0"
|
||||||
|
|
||||||
dotenv-expand@^5.1.0:
|
dotenv-expand@^5.1.0:
|
||||||
version "5.1.0"
|
version "5.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-5.1.0.tgz#3fbaf020bfd794884072ea26b1e9791d45a629f0"
|
resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-5.1.0.tgz#3fbaf020bfd794884072ea26b1e9791d45a629f0"
|
||||||
integrity sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==
|
integrity sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==
|
||||||
|
|
||||||
|
dotenv@^10.0.0:
|
||||||
|
version "10.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-10.0.0.tgz#3d4227b8fb95f81096cdd2b66653fb2c7085ba81"
|
||||||
|
integrity sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==
|
||||||
|
|
||||||
dotenv@^8.2.0:
|
dotenv@^8.2.0:
|
||||||
version "8.2.0"
|
version "8.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a"
|
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a"
|
||||||
|
|
@ -4985,6 +5056,14 @@ electron-publish@22.9.1:
|
||||||
lazy-val "^1.0.4"
|
lazy-val "^1.0.4"
|
||||||
mime "^2.4.6"
|
mime "^2.4.6"
|
||||||
|
|
||||||
|
electron-store@^8.0.0:
|
||||||
|
version "8.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/electron-store/-/electron-store-8.0.0.tgz#81a4e687958e2dae1c5c84cc099a8148be776337"
|
||||||
|
integrity sha512-ZgRPUZkfrrjWSqxZeaxu7lEvmYf6tgl49dLMqxXGnEmliSiwv3u4rJPG+mH3fBQP9PBqgSh4TCuxHZImMMUgWg==
|
||||||
|
dependencies:
|
||||||
|
conf "^10.0.0"
|
||||||
|
type-fest "^1.0.2"
|
||||||
|
|
||||||
electron-to-chromium@^1.3.585:
|
electron-to-chromium@^1.3.585:
|
||||||
version "1.3.589"
|
version "1.3.589"
|
||||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.589.tgz#bd26183ed8697dde6ac19acbc16a3bf33b1f8220"
|
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.589.tgz#bd26183ed8697dde6ac19acbc16a3bf33b1f8220"
|
||||||
|
|
@ -5091,6 +5170,11 @@ env-paths@^2.2.0:
|
||||||
resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.0.tgz#cdca557dc009152917d6166e2febe1f039685e43"
|
resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.0.tgz#cdca557dc009152917d6166e2febe1f039685e43"
|
||||||
integrity sha512-6u0VYSCo/OW6IoD5WCLLy9JUGARbamfSavcNXry/eu8aHVFei6CD3Sw+VGX5alea1i9pgPHW0mbu6Xj0uBh7gA==
|
integrity sha512-6u0VYSCo/OW6IoD5WCLLy9JUGARbamfSavcNXry/eu8aHVFei6CD3Sw+VGX5alea1i9pgPHW0mbu6Xj0uBh7gA==
|
||||||
|
|
||||||
|
env-paths@^2.2.1:
|
||||||
|
version "2.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2"
|
||||||
|
integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==
|
||||||
|
|
||||||
errno@^0.1.3, errno@~0.1.7:
|
errno@^0.1.3, errno@~0.1.7:
|
||||||
version "0.1.7"
|
version "0.1.7"
|
||||||
resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618"
|
resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618"
|
||||||
|
|
@ -5871,6 +5955,11 @@ follow-redirects@^1.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.0.tgz#b42e8d93a2a7eea5ed88633676d6597bc8e384db"
|
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.0.tgz#b42e8d93a2a7eea5ed88633676d6597bc8e384db"
|
||||||
integrity sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA==
|
integrity sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA==
|
||||||
|
|
||||||
|
follow-redirects@^1.10.0:
|
||||||
|
version "1.14.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.1.tgz#d9114ded0a1cfdd334e164e6662ad02bfd91ff43"
|
||||||
|
integrity sha512-HWqDgT7ZEkqRzBvc2s64vSZ/hfOceEol3ac/7tKwzuvEyWx3/4UegXh5oBOIotkGsObyk3xznnSRVADBgWSQVg==
|
||||||
|
|
||||||
for-in@^1.0.2:
|
for-in@^1.0.2:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
|
resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
|
||||||
|
|
@ -8043,6 +8132,16 @@ json-schema-traverse@^0.4.1:
|
||||||
resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
|
resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
|
||||||
integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
|
integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
|
||||||
|
|
||||||
|
json-schema-traverse@^1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2"
|
||||||
|
integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==
|
||||||
|
|
||||||
|
json-schema-typed@^7.0.3:
|
||||||
|
version "7.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/json-schema-typed/-/json-schema-typed-7.0.3.tgz#23ff481b8b4eebcd2ca123b4fa0409e66469a2d9"
|
||||||
|
integrity sha512-7DE8mpG+/fVw+dTpjbxnx47TaMnDfOI1jwft9g1VybltZCduyRQPJPvc+zzKY9WPHxhPWczyFuYa6I8Mw4iU5A==
|
||||||
|
|
||||||
json-schema@0.2.3:
|
json-schema@0.2.3:
|
||||||
version "0.2.3"
|
version "0.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13"
|
resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13"
|
||||||
|
|
@ -8758,6 +8857,11 @@ mimic-fn@^2.1.0:
|
||||||
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
|
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
|
||||||
integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
|
integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
|
||||||
|
|
||||||
|
mimic-fn@^3.0.0:
|
||||||
|
version "3.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-3.1.0.tgz#65755145bbf3e36954b949c16450427451d5ca74"
|
||||||
|
integrity sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==
|
||||||
|
|
||||||
mimic-response@^1.0.0, mimic-response@^1.0.1:
|
mimic-response@^1.0.0, mimic-response@^1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b"
|
resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b"
|
||||||
|
|
@ -9822,6 +9926,13 @@ pkg-dir@^4.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
find-up "^4.0.0"
|
find-up "^4.0.0"
|
||||||
|
|
||||||
|
pkg-up@^3.1.0:
|
||||||
|
version "3.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-3.1.0.tgz#100ec235cc150e4fd42519412596a28512a0def5"
|
||||||
|
integrity sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==
|
||||||
|
dependencies:
|
||||||
|
find-up "^3.0.0"
|
||||||
|
|
||||||
please-upgrade-node@^3.1.1:
|
please-upgrade-node@^3.1.1:
|
||||||
version "3.2.0"
|
version "3.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz#aeddd3f994c933e4ad98b99d9a556efa0e2fe942"
|
resolved "https://registry.yarnpkg.com/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz#aeddd3f994c933e4ad98b99d9a556efa0e2fe942"
|
||||||
|
|
@ -10765,6 +10876,11 @@ require-directory@^2.1.1:
|
||||||
resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
|
resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
|
||||||
integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I=
|
integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I=
|
||||||
|
|
||||||
|
require-from-string@^2.0.2:
|
||||||
|
version "2.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909"
|
||||||
|
integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==
|
||||||
|
|
||||||
require-main-filename@^2.0.0:
|
require-main-filename@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b"
|
resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b"
|
||||||
|
|
@ -11116,7 +11232,7 @@ semver@^7.2.1, semver@^7.3.2:
|
||||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938"
|
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938"
|
||||||
integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==
|
integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==
|
||||||
|
|
||||||
semver@^7.3.4:
|
semver@^7.3.4, semver@^7.3.5:
|
||||||
version "7.3.5"
|
version "7.3.5"
|
||||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7"
|
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7"
|
||||||
integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==
|
integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==
|
||||||
|
|
@ -12388,6 +12504,11 @@ type-fest@^0.8.1:
|
||||||
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d"
|
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d"
|
||||||
integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==
|
integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==
|
||||||
|
|
||||||
|
type-fest@^1.0.2:
|
||||||
|
version "1.1.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-1.1.3.tgz#ea1a602e98e5a968a56a289886a52f04c686fc81"
|
||||||
|
integrity sha512-CsiQeFMR1jZEq8R+H59qe+bBevnjoV5N2WZTTdlyqxeoODQOOepN2+msQOywcieDq5sBjabKzTn3U+sfHZlMdw==
|
||||||
|
|
||||||
type-is@~1.6.17, type-is@~1.6.18:
|
type-is@~1.6.17, type-is@~1.6.18:
|
||||||
version "1.6.18"
|
version "1.6.18"
|
||||||
resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
|
resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue