Compare commits
3
.browserslistrc
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
> 1%
|
||||
last 2 versions
|
||||
not dead
|
||||
38
.eslintrc.js
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
node: true
|
||||
},
|
||||
extends: [
|
||||
"plugin:vue/vue3-essential",
|
||||
"eslint:recommended",
|
||||
"@vue/typescript/recommended"
|
||||
],
|
||||
parserOptions: {
|
||||
ecmaVersion: 2020
|
||||
},
|
||||
rules: {
|
||||
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
|
||||
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
|
||||
"no-unused-vars": "off",
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"error",
|
||||
{ "argsIgnorePattern": "^_" }
|
||||
]
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: [
|
||||
"**/__tests__/*.{j,t}s?(x)",
|
||||
"**/tests/unit/**/*.spec.{j,t}s?(x)",
|
||||
"*.js"
|
||||
],
|
||||
rules: {
|
||||
"@typescript-eslint/no-var-requires": "off",
|
||||
},
|
||||
env: {
|
||||
jest: true
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
30
.gitignore
vendored
|
|
@ -1,5 +1,29 @@
|
|||
.DS_Store
|
||||
/node_modules
|
||||
/out
|
||||
node_modules
|
||||
/dist
|
||||
/crate/target/*
|
||||
/.log
|
||||
|
||||
*.sublime-workspace
|
||||
rawAudio.wav
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
#Electron-builder output
|
||||
/dist_electron
|
||||
|
|
|
|||
2
.npmrc
|
|
@ -1,2 +0,0 @@
|
|||
@crimata:registry=https://gitlab.com/api/v4/projects/28849281/packages/npm/
|
||||
//gitlab.com/api/v4/projects/28849281/packages/npm
|
||||
29
README.md
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# crimata-electron
|
||||
|
||||
## Project setup
|
||||
```
|
||||
yarn install
|
||||
```
|
||||
|
||||
### Compiles and hot-reloads for development
|
||||
```
|
||||
yarn dev
|
||||
```
|
||||
|
||||
### Compiles and minifies for production
|
||||
```
|
||||
yarn electron:build
|
||||
```
|
||||
|
||||
### Run your unit tests
|
||||
```
|
||||
yarn test:unit
|
||||
```
|
||||
|
||||
### Lints and fixes files
|
||||
```
|
||||
yarn lint
|
||||
```
|
||||
|
||||
### Customize configuration
|
||||
See [Configuration Reference](https://cli.vuejs.org/config/).
|
||||
5
babel.config.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
module.exports = {
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset'
|
||||
]
|
||||
}
|
||||
79
build/config.gypi
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
# 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
|
||||
}
|
||||
}
|
||||
4
crash.log
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
PID 335844 received SIGSEGV for address: 0xc
|
||||
/home/enrique/Developer/electron-app/node_modules/segfault-handler/build/Release/segfault-handler.node(+0x37a5)[0x7f1c071387a5]
|
||||
/lib/x86_64-linux-gnu/libpthread.so.0(+0x153c0)[0x7f1c10ffa3c0]
|
||||
/home/enrique/Developer/electron-app/node_modules/electron/dist/electron dist_electron(+0x72d9550)[0x55ccd61fe550]
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
{
|
||||
"folders":
|
||||
[
|
||||
{
|
||||
"path": "."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.debugger</key>
|
||||
<true/>
|
||||
<key>com.apple.security.device.audio-input</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
<!--<key>com.apple.security.cs.disable-library-validation</key>
|
||||
<true/>-->
|
||||
12
gcloud.json
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"type": "service_account",
|
||||
"project_id": "crimata",
|
||||
"private_key_id": "3a324fa3fcca4cea589e4b5007f4142db4b3a84d",
|
||||
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCxFJOjhIkYlyhn\nHjtpFC5EOwzNFZem6k6SS7zcgWbNLe9mcBsiTKCjpxItzkIo0pnT32yqw+AKsqpv\n6+LVyzzjfB7vtEUflvg6KgmYMS4ixOTCEbN3e2qj1t2RnITXZR2VGSGdoKKyqZm0\ntWrmexQvSN20cByL1vfxD3E69oYFGhVraa0zmm94WXHAzofT6DkvGdrbK1s1G9jI\nG3mdQ1jN71tghyloeHL+R/1iKQoZFYTkVe6yWvccSNAZelt/5sciCKRTCArXj89X\nLwU6jGOKIsyqx3BeUyWaSS59xpnGX3meSf5Q9U+j45ePUiqO6TTleF+kWiBwjUpp\nfeYcwEIDAgMBAAECggEAElkN9R7p75zV6F1XDYu0QYiWyncmx/o2Gu1zC6vywWa1\ni/korpSe/mX0ub9J1p3/p1balRUHlUQu6brYvYs56140fGTC1sOXQ7uQU+8glySs\niTk5TbOBeKluOsSrdP/6oTTB6Wm4AegVz4YOpgPxsvaLVkNZidnDxfvyIQhjGYsO\nsvRaw48eEmGOa1uvDhgvPOxiDp5YEdxqSue/7ylzjBdKecDyLgTSDDf/p+FwIx5B\n2d/0uRgQBaAk6D/DNOvN8GrYyakFmlDjE72nrBGAJq50UyV6THOSDk+8cK+2QiS1\nEDCFufj+7Dk1EBfBFUFY8ZBB/pphpEWyLoFl7NzGCQKBgQDaj/KOOcj6BkkTj1WM\nRW3KhOE3nhsCQEgQGCflpycEAZj6J7ktBG2/4teDt9sr+Epx8ymnQCZa9pmysdrD\nU0aWku1qUx0IUKTtZbAEbB+AnrWYDYkIWU8kRUUvNHNTBg43VyuohV9wprPih138\nHWMeNac82/XICz2A3rv+Xkx8KwKBgQDPaaEEGWVs3nIhf/p3M3/xWNKt8fhAERpG\n0YpTjgHW3FQFYjiCTTIcuLO3qsDGmfIAB9tL37s2Y3e2wKBGUj/o69Fr7sWVPdsX\nJtUc0+KiG2Bs0hsyZPdk9UQiQX8y2eq1NZW7vEoxWso1bM64WURB5N8ocWePNJJx\nL2Oe75rtiQKBgQDLfQQOiRxmNF3rOSMkAywyRr9NQgXRdbniSiszNQotP7OHDF7q\n29m2suOGfjIv4O6m4wdf8WkEfd4hsleETc9Ft6wVtyYrrLGxWWCk7WnzHVDjLY7s\n2AHIOjostf+9R8EKoz1BnFN8laibev71EQNMiBWZow1VX6m2hymurWs2mwKBgQCb\nuY3n2v14qOb92e1+U89KsEq1yMd/qpeU9jwqEaO14wS+aglNY5ItWEuuqWhFdE3q\n0ftHUzpnUnUOZD+xrI1JXsyEgegc7i0xi7lUBI3S8kUKTxGWW5IXXcKDCbPrxQtg\ndFPweSUnOyg4xnHKnVMPOjyGS+bZ8TnF+zOLoBAtKQKBgF6hAOIwF1DheXT4p8xS\nlSGhl8dBxjsJDVwbnFWQceGzGnq1e3ncy/UItt42UCDVRWTH4r7pQ9eKyd/3foUD\n6KDIcCrYo2KJh6ZNFJ67XF30U+TuEg5U/9jTbWqDOwYVXcMcnzQE4ZiGBxVHL6xr\nNc8FRW5+7xh4zNfzhGUGt5kt\n-----END PRIVATE KEY-----\n",
|
||||
"client_email": "[email protected]",
|
||||
"client_id": "102914612880037864172",
|
||||
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
||||
"token_uri": "https://oauth2.googleapis.com/token",
|
||||
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
||||
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/crimata-service-account%40crimata.iam.gserviceaccount.com"
|
||||
}
|
||||
BIN
icon.png
|
Before Width: | Height: | Size: 55 KiB |
6
jest.config.js
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
module.exports = {
|
||||
preset: '@vue/cli-plugin-unit-jest/presets/typescript-and-babel',
|
||||
transform: {
|
||||
'^.+\\.vue$': 'vue-jest'
|
||||
}
|
||||
}
|
||||
6
jsconfig.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"typeAcquisition": {
|
||||
|
||||
"enable": true
|
||||
}
|
||||
}
|
||||
45
login.svg
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
<svg
|
||||
id="login"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="350"
|
||||
height="500"
|
||||
>
|
||||
<g transform="translate(-4423 -283)">
|
||||
<rect
|
||||
id="background"
|
||||
width="350"
|
||||
height="500"
|
||||
transform="translate(4423 283)"
|
||||
fill="#ebebeb"
|
||||
/>
|
||||
|
||||
<!-- login Form -->
|
||||
<g id="loginForm" data-name="Group 540" transform="translate(-7 1)">
|
||||
|
||||
<!-- email Box -->
|
||||
<rect
|
||||
id="emailBox"
|
||||
width="161"
|
||||
height="41"
|
||||
rx="10"
|
||||
transform="translate(4525 470)"
|
||||
fill="#b9b9b9"/>
|
||||
|
||||
<!-- password Box -->
|
||||
<rect id="passwordBox"
|
||||
width="161"
|
||||
height="41"
|
||||
rx="10"
|
||||
transform="translate(4525 538)"
|
||||
fill="#b9b9b9"
|
||||
/>
|
||||
|
||||
<text id="Email" transform="translate(4530 523)" font-size="11" font-family="SFCompactDisplay-Bold, SF Compact Display" font-weight="700"><tspan x="0" y="0">Email</tspan></text>
|
||||
<text id="Password" transform="translate(4530 591)" font-size="11" font-family="SFCompactDisplay-Bold, SF Compact Display" font-weight="700"><tspan x="0" y="0">Password</tspan></text>
|
||||
</g>
|
||||
<text id="Login" transform="translate(4518 450)" font-size="20" font-family="SFProText-Bold, SF Pro Text" font-weight="700"><tspan x="0" y="0">Login</tspan></text>
|
||||
<rect id="Rectangle_1493" data-name="Rectangle 1493" width="120" height="38" rx="19" transform="translate(4538 659)" fill="#58c4fd"/>
|
||||
<text id="Submit" transform="translate(4571 684)" fill="#fff" font-size="16" font-family="SFCompactDisplay-Bold, SF Compact Display" font-weight="700"><tspan x="0" y="0">Submit</tspan></text>
|
||||
<text id="Create_account" data-name="Create account" transform="translate(4560 628)" fill="#58c4fd" font-size="11" font-family="SFCompactDisplay-Bold, SF Compact Display" font-weight="700"><tspan x="0" y="0">Create account</tspan></text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
13151
package-lock.json
generated
117
package.json
|
|
@ -1,54 +1,77 @@
|
|||
{
|
||||
"name": "crimata",
|
||||
"version": "1.0.0",
|
||||
"description": "Cross-platform messenger app (Electron, Vue3)",
|
||||
"main": "src/main.js",
|
||||
"name": "Crimata",
|
||||
"productName": "crimata-messenger",
|
||||
"description": "Cross-platform messenger application built with electron, vue3, and TS.",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"author": {
|
||||
"name": "Enrique Hernandez"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "electron-forge start",
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"package": "electron-forge package",
|
||||
"make": "electron-forge make"
|
||||
},
|
||||
"author": "Andrew Gundersen",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@electron-forge/cli": "^6.0.0-beta.63",
|
||||
"@electron-forge/maker-deb": "^6.0.0-beta.63",
|
||||
"@electron-forge/maker-rpm": "^6.0.0-beta.63",
|
||||
"@electron-forge/maker-squirrel": "^6.0.0-beta.63",
|
||||
"@electron-forge/maker-zip": "^6.0.0-beta.63",
|
||||
"electron": "^18.2.3"
|
||||
"dev": "yarn electron:serve",
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"test:unit": "vue-cli-service test:unit",
|
||||
"lint": "vue-cli-service lint",
|
||||
"electron:build": "vue-cli-service electron:build",
|
||||
"electron:serve": "vue-cli-service electron:serve",
|
||||
"postinstall": "electron-builder install-app-deps",
|
||||
"postuninstal": "electron-builder install-app-deps"
|
||||
},
|
||||
"main": "background.js",
|
||||
"dependencies": {
|
||||
"axios": "^0.21.1",
|
||||
"base64-arraybuffer": "^1.0.1",
|
||||
"electron-store": "^8.0.0",
|
||||
"ws": "^7.3.1",
|
||||
"electron-squirrel-startup": "^1.0.0"
|
||||
"@google-cloud/speech": "^4.2.0",
|
||||
"@types/animejs": "^3.1.2",
|
||||
"@types/bindings": "^1.3.0",
|
||||
"@types/dom-mediacapture-record": "^1.0.7",
|
||||
"@types/node": "^14.14.25",
|
||||
"@types/uuid": "^8.3.0",
|
||||
"@types/ws": "^7.2.7",
|
||||
"animejs": "^3.2.0",
|
||||
"core-js": "^3.6.5",
|
||||
"mitt": "^2.1.0",
|
||||
"naudiodon": "^2.3.2",
|
||||
"node-record-lpcm16": "^1.0.1",
|
||||
"vue": "^3.0.0-0",
|
||||
"vue-router": "^4.0.0-0",
|
||||
"vuex": "^4.0.0-0",
|
||||
"ws": "^7.3.1"
|
||||
},
|
||||
"config": {
|
||||
"forge": {
|
||||
"packagerConfig": {},
|
||||
"makers": [
|
||||
{
|
||||
"name": "@electron-forge/maker-squirrel",
|
||||
"config": {}
|
||||
},
|
||||
{
|
||||
"name": "@electron-forge/maker-zip",
|
||||
"platforms": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "@electron-forge/maker-deb",
|
||||
"config": {}
|
||||
},
|
||||
{
|
||||
"name": "@electron-forge/maker-rpm",
|
||||
"config": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
"devDependencies": {
|
||||
"@types/jest": "^24.0.19",
|
||||
"@typescript-eslint/eslint-plugin": "^2.33.0",
|
||||
"@typescript-eslint/parser": "^2.33.0",
|
||||
"@vue/cli-plugin-babel": "~4.5.0",
|
||||
"@vue/cli-plugin-eslint": "~4.5.0",
|
||||
"@vue/cli-plugin-router": "~4.5.0",
|
||||
"@vue/cli-plugin-typescript": "~4.5.0",
|
||||
"@vue/cli-plugin-unit-jest": "~4.5.0",
|
||||
"@vue/cli-plugin-vuex": "~4.5.0",
|
||||
"@vue/cli-service": "~4.5.0",
|
||||
"@vue/compiler-sfc": "^3.0.0-0",
|
||||
"@vue/eslint-config-typescript": "^5.0.2",
|
||||
"@vue/test-utils": "^2.0.0-0",
|
||||
"@wasm-tool/wasm-pack-plugin": "^1.3.1",
|
||||
"electron": "^9.0.0",
|
||||
"electron-devtools-installer": "^3.1.0",
|
||||
"eslint": "^6.7.2",
|
||||
"eslint-plugin-vue": "^7.0.0-0",
|
||||
"lint-staged": "^9.5.0",
|
||||
"node-sass": "^4.12.0",
|
||||
"optimize-wasm-webpack-plugin": "^1.0.12",
|
||||
"sass-loader": "^8.0.2",
|
||||
"spectron": "^11.0.0",
|
||||
"typescript": "~3.9.3",
|
||||
"vue-cli-plugin-electron-builder": "~2.0.0-rc.4",
|
||||
"vue-jest": "^5.0.0-0"
|
||||
},
|
||||
"gitHooks": {
|
||||
"pre-commit": "lint-staged"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,jsx,vue,ts,tsx}": [
|
||||
"vue-cli-service lint",
|
||||
"git add"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
21
public/index.html
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</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>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
</html>
|
||||
122
src/App.vue
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
<template>
|
||||
<div id="app" v-if="!loading">
|
||||
|
||||
<button class="titlebar">
|
||||
|
||||
</button>
|
||||
|
||||
<!-- Menu -->
|
||||
<span class="menu">
|
||||
<button class="menuButton exitButton" @click.prevent="callNavbar('close')"></button>
|
||||
<button class="menuButton minimizeButton" @click.prevent="callNavbar('min')"></button>
|
||||
</span>
|
||||
|
||||
<!-- Windows -->
|
||||
<router-view/>
|
||||
|
||||
</div>
|
||||
<Splash v-else />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent} from 'vue';
|
||||
import { useIpc } from "@/modules/ipc";
|
||||
import { useAuth } from "@/modules/auth";
|
||||
import Splash from "@/components/splash.vue"
|
||||
|
||||
export default defineComponent({
|
||||
components: { Splash },
|
||||
|
||||
setup() {
|
||||
|
||||
const { invoke } = useIpc();
|
||||
const { loading } = useAuth();
|
||||
|
||||
const callNavbar = (action: string) => {
|
||||
invoke('nav-bar', action);
|
||||
}
|
||||
|
||||
return {
|
||||
callNavbar,
|
||||
loading
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
</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;
|
||||
|
||||
background-color: #EBEBEB;
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
.titlebar {
|
||||
position: fixed;
|
||||
|
||||
width: 100vw;
|
||||
height: 54px;
|
||||
|
||||
opacity: 0.75;
|
||||
|
||||
background-color: #EBEBEB;
|
||||
|
||||
-webkit-user-select: none;
|
||||
-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>
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
const { ipcMain } = require("electron");
|
||||
|
||||
const { post } = require("./api");
|
||||
const store = require("./utils/store");
|
||||
const { launchSession, endSession } = require("./session");
|
||||
const { backgroundMitt, ipcEmit } = require("./utils/emitter");
|
||||
|
||||
store.set("account", process.env.ACCOUNT);
|
||||
|
||||
async function auth(_e, creds)
|
||||
{
|
||||
const { error, data } = await post("/auth", creds);
|
||||
|
||||
if (error)
|
||||
{
|
||||
return data;
|
||||
}
|
||||
|
||||
store.set("account", data);
|
||||
ipcEmit("account", data);
|
||||
launchSession(data);
|
||||
};
|
||||
|
||||
function logout(_e, reason)
|
||||
{
|
||||
store.delete("account");
|
||||
ipcEmit("account", false, reason);
|
||||
endSession();
|
||||
};
|
||||
|
||||
function initAccount()
|
||||
{
|
||||
ipcMain.handle("auth", auth);
|
||||
ipcMain.on("logout", logout);
|
||||
|
||||
const account = store.get("account");
|
||||
if (account) launchSession(account);
|
||||
}
|
||||
|
||||
backgroundMitt.on("logout", (reason) => logout(null, reason));
|
||||
|
||||
|
||||
exports.initAccount = initAccount;
|
||||
38
src/api.js
|
|
@ -1,38 +0,0 @@
|
|||
const axios = require('axios').default;
|
||||
|
||||
const config = require("./config");
|
||||
|
||||
|
||||
async function post(route, body)
|
||||
{
|
||||
let error;
|
||||
let data;
|
||||
|
||||
try
|
||||
{
|
||||
const res = await axios.post(config.API + route, body);
|
||||
data = res.data;
|
||||
error = false;
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
if (e.response)
|
||||
{
|
||||
data = e.response.data;
|
||||
}
|
||||
else if (e.request)
|
||||
{
|
||||
data = "Can't connect to the server."
|
||||
}
|
||||
else
|
||||
{
|
||||
data = "Unknown error occured."
|
||||
}
|
||||
|
||||
error = true;
|
||||
}
|
||||
|
||||
return { error, data };
|
||||
}
|
||||
|
||||
exports.post = post;
|
||||
14
src/assets/crimata.svg
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<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>
|
||||
|
After Width: | Height: | Size: 973 B |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 4 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 4.6 KiB |
134
src/audio.js
|
|
@ -1,134 +0,0 @@
|
|||
const nodeAudio = require("@crimata/nodeaudio");
|
||||
const { globalShortcut, ipcMain } = require("electron");
|
||||
|
||||
const { sendMessage } = require("./io");
|
||||
const { updateTray } = require("./tray");
|
||||
const { encode, decode } = require("./codec");
|
||||
const { backgroundMitt, ipcEmit } = require("./utils/emitter");
|
||||
|
||||
let inputDevice;
|
||||
let outputDevice;
|
||||
|
||||
let setWriteId;
|
||||
let setStreamsId;
|
||||
|
||||
let autoStopId; /* keep track of recording time */
|
||||
|
||||
let playbackId; /* UID of the message being played */
|
||||
|
||||
const streamState = { rec: false, pb: false };
|
||||
|
||||
const /** @type {Int16Array[]} */ chunks = [];
|
||||
|
||||
backgroundMitt.on("data", (int16Arr) => {
|
||||
if (streamState.rec) chunks.push(int16Arr);
|
||||
});
|
||||
|
||||
backgroundMitt.on("write", (int16Arr) => {
|
||||
clearTimeout(setWriteId);
|
||||
setWriteId = setTimeout(() => {
|
||||
setPlaybackStatus(false);
|
||||
}, 500);
|
||||
});
|
||||
|
||||
function setStreams()
|
||||
{
|
||||
const defaultInput = nodeAudio.core.GetDefaultInputDevice();
|
||||
const defaultOutput = nodeAudio.core.GetDefaultOutputDevice();
|
||||
|
||||
if (inputDevice !== defaultInput)
|
||||
{
|
||||
inputDevice = defaultInput;
|
||||
nodeAudio.core.CloseInputStream(inputDevice);
|
||||
nodeAudio.core.OpenInputStream(inputDevice);
|
||||
}
|
||||
|
||||
if (outputDevice !== defaultOutput)
|
||||
{
|
||||
outputDevice = defaultOutput;
|
||||
nodeAudio.core.CloseOutputStream(outputDevice);
|
||||
nodeAudio.core.OpenOutputStream(outputDevice);
|
||||
}
|
||||
}
|
||||
|
||||
function startRecording()
|
||||
{
|
||||
setRecordingStatus(true);
|
||||
|
||||
/* 15s recording time limit */
|
||||
autoStopId = setTimeout(stopRecording, 15000);
|
||||
}
|
||||
|
||||
function stopRecording()
|
||||
{
|
||||
if (autoStopId)
|
||||
{
|
||||
clearTimeout(autoStopId);
|
||||
}
|
||||
|
||||
setRecordingStatus(false);
|
||||
|
||||
sendMessage({
|
||||
category: "audio",
|
||||
text: null,
|
||||
blob: encode(nodeAudio.utils.mergeChunks(chunks).buffer)
|
||||
});
|
||||
|
||||
chunks.length = 0;
|
||||
}
|
||||
|
||||
function initAudio()
|
||||
{
|
||||
nodeAudio.core.Initialize(backgroundMitt.emit.bind(backgroundMitt));
|
||||
setStreamsId = setInterval(setStreams, 2000);
|
||||
|
||||
const res = globalShortcut.register('CommandOrControl+Return', () => {
|
||||
streamState.rec ? stopRecording() : startRecording();
|
||||
});
|
||||
|
||||
if (!res) throw new Error("Failed to register recording shortcut");
|
||||
}
|
||||
|
||||
function playback(base64String, id)
|
||||
{
|
||||
/* terminate any current playback */
|
||||
nodeAudio.core.CancelPlayback();
|
||||
ipcEmit("playback", playbackId, false);
|
||||
|
||||
playbackId = id;
|
||||
setPlaybackStatus(true);
|
||||
nodeAudio.core.WriteToOutputStream(decode(base64String));
|
||||
}
|
||||
|
||||
function terminateAudio()
|
||||
{
|
||||
globalShortcut.unregisterAll();
|
||||
|
||||
/* Only terminate PA if initialized */
|
||||
if (setStreamsId)
|
||||
{
|
||||
clearInterval(setStreamsId);
|
||||
nodeAudio.core.Terminate();
|
||||
}
|
||||
}
|
||||
|
||||
function setRecordingStatus(status)
|
||||
{
|
||||
streamState.rec = status;
|
||||
ipcEmit("record", status);
|
||||
updateTray("recording", streamState.rec);
|
||||
}
|
||||
|
||||
function setPlaybackStatus(status)
|
||||
{
|
||||
console.log("Setting playback status: ", status);
|
||||
|
||||
streamState.pb = status;
|
||||
ipcEmit("playback", playbackId, status);
|
||||
updateTray("playback", status);
|
||||
}
|
||||
|
||||
exports.initAudio = initAudio;
|
||||
exports.terminateAudio = terminateAudio;
|
||||
exports.playback = playback;
|
||||
exports.streamState = streamState;
|
||||
25
src/background.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Entry point for Crimata electron app.
|
||||
* "Look on my Works, ye Mighty, and despair!"
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
import { protocol } from "electron";
|
||||
import { initApp } from './background/init';
|
||||
|
||||
// Scheme must be registered before the app is ready
|
||||
protocol.registerSchemesAsPrivileged([
|
||||
{ scheme: "app", privileges: { secure: true, standard: true } }
|
||||
]);
|
||||
|
||||
// Load environment variable
|
||||
const isDevelopment = process.env.NODE_ENV !== "production";
|
||||
|
||||
// NOTE Program Begins Here
|
||||
(async () => {
|
||||
|
||||
console.log('Starting Crimata electron app.');
|
||||
initApp(isDevelopment);
|
||||
|
||||
})();
|
||||
132
src/background/audio.ts
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
/* eslint @typescript-eslint/no-var-requires: "off" */
|
||||
|
||||
"use strict";
|
||||
|
||||
import { sendAudio } from './session'
|
||||
import { ipcMain } from "electron";
|
||||
import { backgroundMitt } from './emitter';
|
||||
const portAudio = require('naudiodon');
|
||||
|
||||
let ai: typeof portAudio.AudioIO | null = null;
|
||||
let ao: typeof portAudio.AudioIO | null = null;
|
||||
let audioInput: Buffer[] = [];
|
||||
const encoding = "base64";
|
||||
const audioOptions = {
|
||||
channelCount: 1,
|
||||
sampleFormat: 16,
|
||||
sampleRate: 16000,
|
||||
deviceId: -1,
|
||||
closeOnError: false,
|
||||
}
|
||||
|
||||
backgroundMitt.once('window-active', (state: boolean) => {
|
||||
if(!state) {
|
||||
stopStream();
|
||||
}
|
||||
});
|
||||
|
||||
// main audio function run by run.ts module.
|
||||
export function initAudioIO(): void {
|
||||
|
||||
if (ai != null) {
|
||||
ai.quit();
|
||||
audioInput = [];
|
||||
}
|
||||
|
||||
ai = new portAudio.AudioIO({ inOptions: audioOptions });
|
||||
|
||||
// base64 encoding needed for google speech to text.
|
||||
ai.setEncoding(encoding);
|
||||
|
||||
// pause the portAudio data flow as soon as stream is initiated.
|
||||
ai.start();
|
||||
ai.pause();
|
||||
|
||||
ai.on('data', (chunk: string) => {
|
||||
// turn string base64 data into buffer object.
|
||||
const buf = Buffer.from(chunk, encoding);
|
||||
audioInput.push(buf);
|
||||
});
|
||||
|
||||
if (ao != null) {
|
||||
ao.end();
|
||||
}
|
||||
ao = new portAudio.AudioIO({ outOptions: audioOptions });
|
||||
ao.start();
|
||||
}
|
||||
|
||||
// callback run on space bar key up and down.
|
||||
const updateRecorder = (_event, record: boolean): void => {
|
||||
if (record) {
|
||||
// resume mic data flow on space bar key down.
|
||||
while(audioInput.length) { audioInput.pop() }
|
||||
ai.resume();
|
||||
} else {
|
||||
// stop mic data flow on space bar key up.
|
||||
ai.pause();
|
||||
const audio = Buffer.concat(audioInput);
|
||||
sendAudio(audio);
|
||||
// clear audioInput.
|
||||
while(audioInput.length) { audioInput.pop() }
|
||||
}
|
||||
}
|
||||
|
||||
// listen for space key up/down event.
|
||||
ipcMain.removeAllListeners('update-recorder');
|
||||
ipcMain.on('update-recorder', updateRecorder);
|
||||
|
||||
// play audio buffers.
|
||||
export function play(input: Array<Buffer>): void {
|
||||
let i = 0;
|
||||
// format buffers into half their size to account for writable highwaterMark.
|
||||
const audio = bufSplit(input);
|
||||
// call this fuction after last audio chunk has been written.
|
||||
const callback = () => {
|
||||
// TODO: clear portAudio writable buffer on write end.
|
||||
}
|
||||
write();
|
||||
// iterate through audio array and write buffers to portAudio writable.
|
||||
function write() {
|
||||
let chunk: Buffer;
|
||||
let ok = true;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function stopStream() {
|
||||
if(ai != null) {
|
||||
ai.quit();
|
||||
ai = null;
|
||||
}
|
||||
if (ao != null) {
|
||||
ao.quit();
|
||||
ao = null;
|
||||
}
|
||||
}
|
||||
|
||||
// utility function used by play func.
|
||||
function bufSplit(input: Array<Buffer>): Array<Buffer> {
|
||||
const result: Buffer[] = [];
|
||||
input.forEach((b: Buffer) => {
|
||||
// split buffer into two.
|
||||
result.push(b.slice(0, b.length / 2), b.slice(b.length / 2, b.length));
|
||||
});
|
||||
return result;
|
||||
}
|
||||
6
src/background/emitter.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
/* eslint-disable */
|
||||
const EventEmitter = require('events');
|
||||
|
||||
class BackgroundMitt extends EventEmitter { }
|
||||
|
||||
export const backgroundMitt = new BackgroundMitt();
|
||||
49
src/background/init.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
import { app } from "electron";
|
||||
import { main } from './run';
|
||||
import { backgroundMitt } from './emitter';
|
||||
|
||||
let winActive: boolean;
|
||||
|
||||
backgroundMitt.once('window-active', (state: boolean) => {
|
||||
winActive = state;
|
||||
});
|
||||
|
||||
export function initApp(dev: boolean): void {
|
||||
|
||||
app.on("ready", () => {
|
||||
main();
|
||||
});
|
||||
|
||||
// Quit when all windows are closed.
|
||||
app.on("window-all-closed", () => {
|
||||
if (process.platform !== "darwin") {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
// Might have to de-reference window object
|
||||
app.on("activate", () => {
|
||||
if (winActive === false) {
|
||||
main();
|
||||
}
|
||||
});
|
||||
|
||||
// Exit cleanly on request from parent process in development mode.
|
||||
if (dev) {
|
||||
if (process.platform === "win32") {
|
||||
process.on("message", data => {
|
||||
if (data === "graceful-exit") {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
process.on("SIGTERM", () => {
|
||||
app.quit();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
28
src/background/run.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
"use strict";
|
||||
|
||||
import { createWindow } from './window';
|
||||
import { initSession } from './session';
|
||||
import { initAudioIO } from './audio';
|
||||
|
||||
let socket = false;
|
||||
|
||||
/*
|
||||
* The main function will be run after electron app is ready.
|
||||
*/
|
||||
export async function main() {
|
||||
|
||||
// create main window.
|
||||
await createWindow({
|
||||
width: 350,
|
||||
height: 500,
|
||||
resizable: true
|
||||
});
|
||||
|
||||
// Instantiate socket session with crimata-platorm.
|
||||
if (!socket) initSession();
|
||||
socket = true;
|
||||
|
||||
// Begin audio stream.
|
||||
initAudioIO();
|
||||
|
||||
}
|
||||
149
src/background/session.ts
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
"use strict";
|
||||
|
||||
import WebSocket from 'ws';
|
||||
import { backgroundMitt } from './emitter';
|
||||
import { ipcMain } from "electron";
|
||||
|
||||
interface RenderMessage {
|
||||
content: {
|
||||
text: string;
|
||||
audio: boolean | Buffer;
|
||||
};
|
||||
avatar: string;
|
||||
context: string;
|
||||
subContext: string;
|
||||
modifier: string;
|
||||
time: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
interface UserCreds {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
interface TextMessage {
|
||||
audio: 0;
|
||||
content: string;
|
||||
}
|
||||
|
||||
const ip = 'ws://127.0.0.1';
|
||||
const port = 8760;
|
||||
const reconnectTimeout = 3000; //ms
|
||||
let socket: WebSocket;
|
||||
let success = false;
|
||||
let auth = false;
|
||||
|
||||
const authSession = async (_event, payload: string | UserCreds | null): Promise<string> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
console.log('authenticating...');
|
||||
|
||||
backgroundMitt.once('auth-res', (res: string) => {
|
||||
|
||||
if (res === 'locked') {
|
||||
reject(res);
|
||||
} else {
|
||||
console.log('Success!');
|
||||
auth = true;
|
||||
resolve(res);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
if (!payload) {
|
||||
socket.send('no-token');
|
||||
} else if (payload instanceof String || typeof payload === 'string') {
|
||||
socket.send(payload);
|
||||
} else {
|
||||
socket.send(JSON.stringify(payload));
|
||||
}
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
const receiveMessage = (message: string): void => {
|
||||
|
||||
while (!auth) {
|
||||
backgroundMitt.emit('auth-res', message);
|
||||
return;
|
||||
}
|
||||
|
||||
const parsed: RenderMessage = JSON.parse(message);
|
||||
console.log('message received', parsed);
|
||||
if (parsed.content.audio) {
|
||||
}
|
||||
backgroundMitt.emit('ipc-renderer', {
|
||||
endpoint: 'render-message',
|
||||
message: parsed
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
const sendMessage = (_event, payload: TextMessage): void => {
|
||||
socket.send(JSON.stringify(payload));
|
||||
}
|
||||
|
||||
export const sendAudio = (content: Buffer) => {
|
||||
socket.send(content);
|
||||
}
|
||||
|
||||
// Run every time we want to connect to backend.
|
||||
export const initSession = () => {
|
||||
|
||||
// Close existing sockets.
|
||||
if (socket) {
|
||||
socket.removeAllListeners();
|
||||
socket.terminate();
|
||||
socket.close();
|
||||
success = false;
|
||||
}
|
||||
|
||||
// Init new socket.
|
||||
socket = new WebSocket(`${ip}:${port}`);
|
||||
socket.binaryType = 'arraybuffer';
|
||||
|
||||
// handle renderer auth-token event
|
||||
ipcMain.removeHandler('auth-session'); // avoid setting duplicate handlers
|
||||
ipcMain.handle('auth-session', authSession);
|
||||
|
||||
// handle user message event
|
||||
ipcMain.removeAllListeners('message');
|
||||
ipcMain.on('message', sendMessage);
|
||||
|
||||
// Connect to the backend.
|
||||
socket.on('open', () => {
|
||||
console.log('Connected to Crimata Servers.');
|
||||
|
||||
// call auth renderer event on connection
|
||||
backgroundMitt.emit('ipc-renderer', {
|
||||
endpoint: 'auth',
|
||||
message: null
|
||||
});
|
||||
|
||||
success = true;
|
||||
|
||||
});
|
||||
|
||||
// Handle for failed connect, try again.
|
||||
socket.on('error', (_e) => {
|
||||
console.log('ERROR: Failed to connect.');
|
||||
socket.removeAllListeners();
|
||||
socket.close();
|
||||
setTimeout(() => {
|
||||
if (!success) {
|
||||
console.log('Reconnecting...');
|
||||
initSession();
|
||||
}
|
||||
}, reconnectTimeout);
|
||||
|
||||
});
|
||||
|
||||
socket.on('close', () => {
|
||||
console.log('Connection droped! Restarting...')
|
||||
initSession();
|
||||
});
|
||||
|
||||
socket.on("message", receiveMessage);
|
||||
};
|
||||
|
||||
113
src/background/window.ts
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
"use strict";
|
||||
|
||||
import { BrowserWindow, ipcMain } from "electron";
|
||||
import { createProtocol } from "vue-cli-plugin-electron-builder/lib";
|
||||
import { backgroundMitt } from './emitter';
|
||||
import * as path from "path";
|
||||
|
||||
interface WindowSettings {
|
||||
width: number;
|
||||
height: number;
|
||||
resizable: boolean;
|
||||
}
|
||||
|
||||
interface RenderMessage {
|
||||
content: string;
|
||||
context: string;
|
||||
subContext: string;
|
||||
modifiers: string;
|
||||
time: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
interface IpcRendererPayload {
|
||||
endpoint: string;
|
||||
message: RenderMessage | null;
|
||||
}
|
||||
|
||||
let win: BrowserWindow | null;
|
||||
|
||||
// Util function to handle window close & minimize.
|
||||
const navBarHandler = (_event, action: string): void => {
|
||||
switch(action) {
|
||||
case 'close':
|
||||
if (win) win.close();
|
||||
break;
|
||||
case 'min':
|
||||
if (win) win.minimize();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Util function to render message on ipc-renderer event.
|
||||
const renderMessage = (payload: IpcRendererPayload): void => {
|
||||
if (win) {
|
||||
win.webContents.send(payload.endpoint, {
|
||||
message: payload.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Do this on window mount.
|
||||
const windowMount = (): void => {
|
||||
backgroundMitt.emit('window-active', true);
|
||||
// handle win nav-bar event.
|
||||
ipcMain.removeHandler('nav-bar'); // avoid setting duplicate handlers
|
||||
ipcMain.handle('nav-bar', navBarHandler);
|
||||
// render messages through ipc-renderer.
|
||||
backgroundMitt.once('ipc-renderer', renderMessage);
|
||||
}
|
||||
|
||||
// do this on window dismount.
|
||||
const windowDismount = (): void => {
|
||||
win = null;
|
||||
backgroundMitt.emit('window-active', false);
|
||||
}
|
||||
|
||||
// function used by run.ts to create the main window.
|
||||
export async function createWindow(options: WindowSettings): Promise<void> {
|
||||
return new Promise((resolve, _reject) => {
|
||||
|
||||
// avoid creating duplicate windows.
|
||||
if (win) resolve();
|
||||
|
||||
win = new BrowserWindow({
|
||||
width: options.width,
|
||||
height: options.height,
|
||||
resizable: options.resizable,
|
||||
backgroundColor: '#EBEBEB',
|
||||
frame: false,
|
||||
minWidth: 350,
|
||||
minHeight: 500,
|
||||
webPreferences: {
|
||||
// Use pluginOptions.nodeIntegration, leave this alone
|
||||
// See nklayman.github.io/vue-cli-plugin-electron-builder/guide/security.html#node-integration for more info
|
||||
nodeIntegration: (process.env
|
||||
.ELECTRON_NODE_INTEGRATION as unknown) as boolean,
|
||||
preload: path.join(__dirname, "preload.js")
|
||||
}
|
||||
});
|
||||
|
||||
if (process.env.WEBPACK_DEV_SERVER_URL) {
|
||||
// Load the url of the dev server if in development mode
|
||||
win.loadURL(process.env.WEBPACK_DEV_SERVER_URL as string);
|
||||
} else {
|
||||
createProtocol("app");
|
||||
// Load the index.html when not in development
|
||||
win.loadURL("app://./index.html");
|
||||
}
|
||||
|
||||
// Handle window close.
|
||||
win.on("closed", windowDismount);
|
||||
|
||||
win.once('ready-to-show', () => {
|
||||
if (win) win.show()
|
||||
})
|
||||
|
||||
win.webContents.on('did-finish-load', () => {
|
||||
windowMount();
|
||||
resolve();
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
13
src/codec.js
|
|
@ -1,13 +0,0 @@
|
|||
const { ipcMain } = require("electron");
|
||||
const { encode, decode } = require("base64-arraybuffer");
|
||||
|
||||
ipcMain.handle("encode", (_e, arrayBuffer) => {
|
||||
return encode(arrayBuffer);
|
||||
});
|
||||
|
||||
ipcMain.handle("decode", (_e, b64string) => {
|
||||
return decode(b64string);
|
||||
});
|
||||
|
||||
exports.encode = encode;
|
||||
exports.decode = decode;
|
||||
140
src/components/messageItem.vue
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
<template>
|
||||
<div
|
||||
class="message"
|
||||
:id="`${message.modifier}Message`"
|
||||
:key="message.id"
|
||||
>
|
||||
|
||||
<!-- Includes bubble and context. -->
|
||||
<div class="messageBox" :id="`${message.modifier}MessageBox`">
|
||||
|
||||
<!-- message bubble -->
|
||||
<div
|
||||
class="bubble"
|
||||
:id="`${message.modifier}Bubble`"
|
||||
>
|
||||
{{ message.content }}
|
||||
</div>
|
||||
|
||||
<!-- message context -->
|
||||
<span class="context">
|
||||
|
||||
<!-- Render Crimata icon or initials. -->
|
||||
<div v-if="message.avatar == 'crimata'" class="photo">
|
||||
<img src="@/assets/crimata.svg"/>
|
||||
</div>
|
||||
|
||||
<div v-else class="photo">
|
||||
{{ message.avatar }}
|
||||
</div>
|
||||
|
||||
{{ message.context }}
|
||||
|
||||
</span>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang='ts'>
|
||||
import {defineComponent} from 'vue';
|
||||
export default defineComponent({
|
||||
name: "MessageItem",
|
||||
props: {
|
||||
message: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
}
|
||||
})
|
||||
</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;
|
||||
}
|
||||
|
||||
#aiMessage {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
#sfMessage {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
#aiMessageBox {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
#sfMessageBox {
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.bubble {
|
||||
max-width: 66vw;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
font-family: "SF Pro Text";
|
||||
font-size: 14px;
|
||||
|
||||
padding: 10px;
|
||||
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
#aiBubble {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
#sfBubble {
|
||||
background-color: #58c4fd;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.context {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
font-family: "SF Compact Display";
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
margin-left: 7px;
|
||||
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;
|
||||
}
|
||||
|
||||
</style>
|
||||
111
src/components/messageView/index.vue
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
<template>
|
||||
<svg
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
:viewBox="scrollView"
|
||||
@click="emittMessage"
|
||||
>
|
||||
<g id="messageView" transform="translate(0, 20)">
|
||||
<g v-for="(item, index) in renderArr" :key="index" preserverAspectRatio="none">
|
||||
<transition
|
||||
appear
|
||||
v-on:before-appear="beforeEnter"
|
||||
v-on:appear="enter"
|
||||
v-on:after-enter="afterEnter"
|
||||
>
|
||||
<TextBubble :item="item" :key="index" />
|
||||
</transition>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import TextBubble from "@/components/textBubble/index.vue";
|
||||
import { Message } from "@/types/message/index";
|
||||
import { useIpc } from "@/modules/ipc";
|
||||
import { Queue } from "@/modules/queue";
|
||||
import useTransitions from "./viewDetails/transitions";
|
||||
import useScrollView from "@/modules/scrollView";
|
||||
import useMitt from "@/modules/mitt";
|
||||
import {
|
||||
defineComponent,
|
||||
reactive,
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
watch,
|
||||
} from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "MessageView",
|
||||
components: { TextBubble },
|
||||
|
||||
setup() {
|
||||
const { emitter } = useMitt();
|
||||
const renderArr: Message[] = reactive([]);
|
||||
const renderQ = new Queue();
|
||||
|
||||
const { invoke } = useIpc("message");
|
||||
|
||||
// Each method will be called on their respective lifecycle event.
|
||||
const { beforeEnter, enter, afterEnter, rendering } = useTransitions();
|
||||
const { scrollView } = useScrollView({
|
||||
targetId: "messageView",
|
||||
});
|
||||
|
||||
const render = (m: Message) => {
|
||||
if (rendering.value) {
|
||||
renderQ.enqueue(m);
|
||||
} else {
|
||||
renderArr.push(m);
|
||||
}
|
||||
};
|
||||
|
||||
watch(rendering, (rendering, _prevRendering) => {
|
||||
if (!rendering) {
|
||||
const m = renderQ.dequeue() as Message | null;
|
||||
if (m) renderArr.push(m);
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
// Call render function on event "render-message."
|
||||
window.ipcRenderer.on("render-message", (event, payload) => {
|
||||
const m = payload.message;
|
||||
if (m.content) render(m);
|
||||
});
|
||||
|
||||
emitter.on("user-message", async (message) => {
|
||||
try {
|
||||
const res = await invoke(message);
|
||||
render(res);
|
||||
} catch (e) {
|
||||
console.log("No response. Do not render.");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.ipcRenderer.removeAllListeners("render-message");
|
||||
emitter.all.clear();
|
||||
});
|
||||
|
||||
return {
|
||||
renderArr,
|
||||
beforeEnter,
|
||||
enter,
|
||||
afterEnter,
|
||||
scrollView,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
#messageView {
|
||||
width: 1000px;
|
||||
height: 100vh;
|
||||
color: red;
|
||||
}
|
||||
</style>
|
||||
69
src/components/splash.vue
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
<template>
|
||||
<div id="splash">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="57.046"
|
||||
height="52.759"
|
||||
viewBox="0 0 57.046 52.759"
|
||||
>
|
||||
<g transform="translate(-4127 -507)">
|
||||
<g transform="translate(3949 332.206)">
|
||||
<g transform="translate(178 174.794)">
|
||||
<g transform="translate(0 27.405)">
|
||||
<circle
|
||||
cx="12.677"
|
||||
cy="12.677"
|
||||
r="12.677"
|
||||
transform="translate(0 0)"
|
||||
fill="#383838"
|
||||
/>
|
||||
<circle
|
||||
cx="12.677"
|
||||
cy="12.677"
|
||||
r="12.677"
|
||||
transform="translate(31.692 0)"
|
||||
fill="#383838"
|
||||
/>
|
||||
</g>
|
||||
<circle
|
||||
cx="12.677"
|
||||
cy="12.677"
|
||||
r="12.677"
|
||||
transform="translate(15.822 0)"
|
||||
fill="#383838"
|
||||
/>
|
||||
<path
|
||||
d="M36.27,83.67"
|
||||
transform="translate(-23.569 -43.588)"
|
||||
fill="#ff0"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
import { defineComponent } from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "Splash",
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
#splash {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
}
|
||||
|
||||
</style>
|
||||
65
src/components/taInput/audioDetails/audio.vue
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
:width="bubbleWidth"
|
||||
height="34"
|
||||
:x="center"
|
||||
y="440"
|
||||
rx="10"
|
||||
class="audioInput"
|
||||
>
|
||||
<g id="Group_126" data-name="Group 126" transform="translate(0 0)" rx="10">
|
||||
<rect
|
||||
id="Rectangle_478"
|
||||
data-name="Rectangle 478"
|
||||
:width="bubbleWidth"
|
||||
height="34"
|
||||
rx="10"
|
||||
transform="translate(0 0)"
|
||||
fill="#b9b9b9"
|
||||
/>
|
||||
<svg>
|
||||
<g
|
||||
rx="20"
|
||||
id="Group_125"
|
||||
data-name="Group 125"
|
||||
v-for="(path, index) in paths"
|
||||
:key="path.identifier"
|
||||
>
|
||||
<path
|
||||
:key="index"
|
||||
id="path"
|
||||
:data-name="path.identifier"
|
||||
:d="path.d"
|
||||
:transform="`translate(${path.offset} 0)`"
|
||||
fill="none"
|
||||
:stroke="path.color"
|
||||
stroke-linecap="round"
|
||||
stroke-width="1.5"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed, inject } from "vue";
|
||||
export default defineComponent({
|
||||
name: "AudioStream",
|
||||
props: {
|
||||
bubbleWidth: Number,
|
||||
paths: Array,
|
||||
},
|
||||
setup(props) {
|
||||
const emitter: any = inject("mitt");
|
||||
|
||||
const center = computed(() => {
|
||||
if (props.bubbleWidth) {
|
||||
return 175 - props.bubbleWidth / 2;
|
||||
}
|
||||
});
|
||||
return { center };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
147
src/components/taInput/audioDetails/audioVisualizer.ts
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
import { Ref } from '@/types/vueRef/index';
|
||||
import AnimeFunc from "@/types/animejs/index";
|
||||
import { inject, ref } from "vue";
|
||||
|
||||
interface AudioPath {
|
||||
identifier: string;
|
||||
d: string;
|
||||
offset: number;
|
||||
color: string;
|
||||
draw: boolean;
|
||||
}
|
||||
export default function useAudioVisualizer(draw: Ref<boolean>) {
|
||||
const audioContext = new AudioContext();
|
||||
const analyzer = audioContext.createAnalyser();
|
||||
analyzer.fftSize = 128;
|
||||
const bufferLength = analyzer.frequencyBinCount;
|
||||
const dataArray = new Uint8Array(bufferLength);
|
||||
|
||||
// imports animejs safely
|
||||
let anime: AnimeFunc;
|
||||
const animeInject: AnimeFunc | undefined = inject("animejs");
|
||||
if (animeInject) anime = animeInject;
|
||||
|
||||
/*
|
||||
* Draws MediaStream audioTrack as 'AudioPath' objects
|
||||
*/
|
||||
function drawStream(paths: Ref<any>, bubbleWidth: Ref<number>): void {
|
||||
const xInitial = ref(10);
|
||||
const maxAmplitude = 15;
|
||||
const maxBubbleWidth = 300;
|
||||
|
||||
function shiftPathStream(pathList: NodeList): void {
|
||||
for (let i = 0; i < pathList.length; i++) {
|
||||
const stick = pathList[i] as HTMLElement;
|
||||
const computedQuery = window.getComputedStyle(stick);
|
||||
const matrix = new WebKitCSSMatrix(computedQuery.webkitTransform);
|
||||
anime({
|
||||
targets: stick,
|
||||
translateX: [matrix.m41, matrix.m41 - 6],
|
||||
})
|
||||
}
|
||||
}
|
||||
function filterPathList(pathList: NodeList) {
|
||||
for (let i = 0; i < pathList.length; i++) {
|
||||
const stick = pathList[i] as HTMLElement;
|
||||
const computedQuery = window.getComputedStyle(stick);
|
||||
const matrix = new WebKitCSSMatrix(computedQuery.webkitTransform);
|
||||
if (matrix.m41 < 10) {
|
||||
anime({
|
||||
targets: stick,
|
||||
opacity: 0,
|
||||
duration: 0
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function drawNewPath(): void {
|
||||
|
||||
const calculateAmplitude = (): number => (
|
||||
Math.min(Math.max(
|
||||
(dataArray.reduce((a, b) => a + b) / bufferLength) * 0.45,
|
||||
0), maxAmplitude)
|
||||
)
|
||||
|
||||
const createAudioPath = (a: number, o: number): AudioPath => ({
|
||||
identifier: `path_${o}`,
|
||||
d: `M0,25v${-a}`,
|
||||
offset: o,
|
||||
color: '#000',
|
||||
draw: true
|
||||
})
|
||||
|
||||
const amplitude = calculateAmplitude();
|
||||
const p: AudioPath = createAudioPath(amplitude, xInitial.value);
|
||||
paths.value.push(p);
|
||||
}
|
||||
|
||||
paths.value = [];
|
||||
function drawPathStream(): void {
|
||||
|
||||
let streamPath;
|
||||
if (!draw.value) {
|
||||
window.clearTimeout(streamPath)
|
||||
return;
|
||||
}
|
||||
setTimeout(() => {
|
||||
requestAnimationFrame(drawPathStream);
|
||||
analyzer.getByteFrequencyData(dataArray);
|
||||
}, 1000 / 10)
|
||||
|
||||
/*
|
||||
* To Draw pathStream
|
||||
*/
|
||||
if (bubbleWidth.value === 300) {
|
||||
xInitial.value = bubbleWidth.value - 10;
|
||||
} else if (bubbleWidth.value > 20) {
|
||||
xInitial.value = bubbleWidth.value;
|
||||
} else {
|
||||
xInitial.value += 3;
|
||||
}
|
||||
streamPath = setTimeout(async () => {
|
||||
const pathList = document.querySelectorAll('path');
|
||||
filterPathList(pathList);
|
||||
drawNewPath();
|
||||
if (bubbleWidth.value === maxBubbleWidth) {
|
||||
shiftPathStream(pathList);
|
||||
}
|
||||
}, 200)
|
||||
}
|
||||
drawPathStream();
|
||||
}
|
||||
|
||||
function increaseBubbleWidth(bubbleRef: Ref<number>) {
|
||||
const increase = () => {
|
||||
|
||||
let animation;
|
||||
if (!draw.value) {
|
||||
if (animation) {
|
||||
cancelAnimationFrame(animation)
|
||||
}
|
||||
return;
|
||||
}
|
||||
animation = requestAnimationFrame(increase);
|
||||
if (bubbleRef.value < 300) {
|
||||
bubbleRef.value++;
|
||||
}
|
||||
}
|
||||
increase();
|
||||
}
|
||||
|
||||
function expandBubble(bubbleWidth: Ref<number>) {
|
||||
increaseBubbleWidth(bubbleWidth);
|
||||
}
|
||||
|
||||
function visualizeStreamAsPaths(s: MediaStream, paths: Ref<any>, bubbleWidth: Ref<number>): void {
|
||||
const source = audioContext.createMediaStreamSource(s);
|
||||
source.connect(analyzer);
|
||||
drawStream(paths, bubbleWidth);
|
||||
}
|
||||
|
||||
return {
|
||||
visualizeStreamAsPaths,
|
||||
expandBubble
|
||||
}
|
||||
}
|
||||
54
src/components/taInput/index.vue
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
<template>
|
||||
|
||||
<!-- render audio or text input-->
|
||||
<AudioStream
|
||||
v-if="visualizeStream"
|
||||
:bubbleWidth="audioBubbleWidth"
|
||||
:paths="paths"
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
v-else
|
||||
:input="input"
|
||||
/>
|
||||
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
// import child components
|
||||
import TextInput from "./textDetails/text.vue";
|
||||
import AudioStream from "./audioDetails/audio.vue";
|
||||
|
||||
import useInputController from "./inputController";
|
||||
|
||||
import { defineComponent } from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
|
||||
name: "TextAudioInput",
|
||||
components: { TextInput, AudioStream },
|
||||
setup() {
|
||||
// get text input
|
||||
|
||||
// get audio stream
|
||||
|
||||
// use Input Handler
|
||||
|
||||
const {
|
||||
input,
|
||||
visualizeStream,
|
||||
audioBubbleWidth,
|
||||
paths,
|
||||
} = useInputController();
|
||||
|
||||
return {
|
||||
input,
|
||||
|
||||
visualizeStream,
|
||||
audioBubbleWidth,
|
||||
paths,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
103
src/components/taInput/inputController.ts
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
/*
|
||||
* Controls which input to visualize
|
||||
* Will visualize text input by default as user types or
|
||||
* if user holds space bar down, input audio stream
|
||||
* will be visualized & recorded
|
||||
*/
|
||||
|
||||
import { ref, watch, onUnmounted, onMounted } from "vue";
|
||||
import useKeyDownHandler from "@/modules/keyDownHandler/useKeyDownHandler";
|
||||
import useMediaStream from "@/modules/mediaStream";
|
||||
import useAudioVisualizer from './audioDetails/audioVisualizer';
|
||||
import useTextVisualizer from './textDetails/textVisualizer';
|
||||
import useMitt from "@/modules/mitt";
|
||||
import { useIpc } from '@/modules/ipc';
|
||||
|
||||
let stream: MediaStream;
|
||||
|
||||
export default function useInputController() {
|
||||
const visualizeStream = ref(false);
|
||||
const paths = ref([]);
|
||||
const audioBubbleWidth = ref(10);
|
||||
const { emitter } = useMitt();
|
||||
const { post } = useIpc();
|
||||
|
||||
// send message on ENTER
|
||||
const enterCallback = (input: string) => {
|
||||
|
||||
const message = {
|
||||
audio: 0,
|
||||
text: input
|
||||
};
|
||||
|
||||
// trigger send animation
|
||||
emitter.emit("animate-send");
|
||||
|
||||
// send message to backend
|
||||
if (input) post('message', message);
|
||||
}
|
||||
|
||||
const {
|
||||
visualizeStreamAsPaths,
|
||||
expandBubble
|
||||
} = useAudioVisualizer(visualizeStream);
|
||||
|
||||
const { keyDownHandler, input } = useKeyDownHandler(enterCallback);
|
||||
|
||||
const {
|
||||
textInputXoffset,
|
||||
textInputYoffset,
|
||||
renderTextInput
|
||||
} = useTextVisualizer(input);
|
||||
|
||||
// stops stream recording on space key up
|
||||
function keyupHandler(e: any) {
|
||||
if (e.key === " " && visualizeStream.value) {
|
||||
|
||||
post('update-recorder', false);
|
||||
visualizeStream.value = false;
|
||||
paths.value = []
|
||||
audioBubbleWidth.value = 10;
|
||||
input.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
stream = await useMediaStream({ audio: true });
|
||||
// add window event keyboard listeners
|
||||
window.addEventListener("keydown", keyDownHandler);
|
||||
window.addEventListener('keyup', keyupHandler);
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener("keydown", keyDownHandler);
|
||||
window.removeEventListener("keyup", keyupHandler);
|
||||
});
|
||||
|
||||
// determine whether to render text or audio based one the keyboard input
|
||||
watch(input, (input, prevInput) => {
|
||||
if (prevInput !== "" || prevInput.length > 0) {
|
||||
if (!visualizeStream.value) {
|
||||
renderTextInput();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (input === " " && visualizeStream.value === false) {
|
||||
visualizeStream.value = true;
|
||||
post('update-recorder', true);
|
||||
expandBubble(audioBubbleWidth);
|
||||
visualizeStreamAsPaths(stream, paths, audioBubbleWidth);
|
||||
return;
|
||||
}
|
||||
renderTextInput();
|
||||
});
|
||||
|
||||
|
||||
return {
|
||||
input,
|
||||
visualizeStream,
|
||||
audioBubbleWidth,
|
||||
paths
|
||||
}
|
||||
|
||||
}
|
||||
64
src/components/taInput/textDetails/text.vue
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
<template>
|
||||
|
||||
<div class="inputBox">
|
||||
<div class="inputBubble" v-show="input.length > 0">
|
||||
<p>{{ input }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
import { defineComponent } from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
|
||||
name: "TextInput",
|
||||
props: {
|
||||
input: String,
|
||||
},
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
.inputBox {
|
||||
position: fixed;
|
||||
|
||||
width: 100vw;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
bottom: 0;
|
||||
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.inputBubble {
|
||||
max-width: 180px;
|
||||
min-height: 32px;
|
||||
|
||||
display: table;
|
||||
justify-items: center;
|
||||
|
||||
border-radius: 10px;
|
||||
background-color: #B9B9B9;
|
||||
|
||||
font-size: 14px;
|
||||
|
||||
p {
|
||||
max-width: 150px;
|
||||
overflow-wrap: break-word;
|
||||
padding: 7px 10px 5px 10px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
margin-bottom: 20px;
|
||||
|
||||
z-index: 1;
|
||||
}
|
||||
</style>
|
||||
57
src/components/taInput/textDetails/textAnime.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import AnimeFunc from "@/types/animejs/index";
|
||||
import { inject } from "vue";
|
||||
import { Ref } from "@/types/vueRef/index";
|
||||
/*
|
||||
* Anime js animations used by input textController
|
||||
*/
|
||||
export default function useTextInputAnims() {
|
||||
|
||||
// imports animejs safely
|
||||
let anime: AnimeFunc;
|
||||
const animeInject: AnimeFunc | undefined = inject("animejs");
|
||||
if (animeInject) anime = animeInject;
|
||||
|
||||
const animateSend = (el: HTMLElement, elHeight: number, input: Ref<string>) => {
|
||||
anime({
|
||||
targets: el,
|
||||
translateY: -elHeight + 15,
|
||||
duration: 200,
|
||||
easing: "easeInQuad",
|
||||
complete: function() {
|
||||
anime({
|
||||
targets: el,
|
||||
translateY: 50,
|
||||
duration: 200,
|
||||
easing: "easeOutQuad",
|
||||
complete: function() {
|
||||
input.value = "";
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const inputAppear = (el: HTMLElement) => {
|
||||
anime({
|
||||
targets: el,
|
||||
translateY: -10,
|
||||
duration: 0
|
||||
});
|
||||
}
|
||||
|
||||
const verticalShiftInput = (el: HTMLElement, yTrans: number) => {
|
||||
anime({
|
||||
targets: el,
|
||||
translateY: yTrans,
|
||||
easing: "easeOutQuad",
|
||||
duration: 150,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
animateSend,
|
||||
verticalShiftInput,
|
||||
inputAppear
|
||||
}
|
||||
|
||||
}
|
||||
62
src/components/taInput/textDetails/textVisualizer.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* text rendering logic used by inputController
|
||||
*/
|
||||
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue';
|
||||
import useTextInputAnims from "./textAnime";
|
||||
import { Ref } from "@/types/vueRef/index";
|
||||
import useMitt from "@/modules/mitt";
|
||||
|
||||
export default function useTextVisualizer(input: Ref<string>) {
|
||||
const inputHeight = ref(42);
|
||||
const inputWidth = ref(0);
|
||||
const { emitter } = useMitt();
|
||||
const { animateSend, verticalShiftInput, inputAppear } = useTextInputAnims();
|
||||
|
||||
async function renderTextInput() {
|
||||
// first, get input dimensions
|
||||
const inputDivs = await document.getElementsByClassName("inputBubble");
|
||||
const height = inputDivs[0].getBoundingClientRect().height;
|
||||
inputWidth.value = inputDivs[0].getBoundingClientRect().width;
|
||||
// then, query for foreignObject div
|
||||
const foreignObjectDiv = document.getElementsByClassName(
|
||||
"inputContainer"
|
||||
);
|
||||
const foreignEl = foreignObjectDiv[0] as HTMLElement;
|
||||
if (height !== inputHeight.value) {
|
||||
inputHeight.value = height;
|
||||
}
|
||||
if (inputHeight.value === 0) {
|
||||
inputAppear(foreignEl)
|
||||
} else if (inputHeight.value > 36) {
|
||||
const yTrans = -inputHeight.value + 30;
|
||||
verticalShiftInput(foreignEl, yTrans);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
emitter.on("animate-send", () => {
|
||||
const inputEl = document.getElementsByClassName("inputContainer");
|
||||
const foreignEl = inputEl[0] as HTMLElement;
|
||||
animateSend(foreignEl, inputHeight.value, input);
|
||||
});
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
emitter.all.clear();
|
||||
});
|
||||
|
||||
const textInputXoffset = computed(() => {
|
||||
return `calc(50% - ${inputWidth.value / 2})`;
|
||||
});
|
||||
|
||||
const textInputYoffset = computed(() => {
|
||||
return `calc(97% - ${inputHeight.value})`;
|
||||
});
|
||||
|
||||
return {
|
||||
textInputXoffset,
|
||||
textInputYoffset,
|
||||
renderTextInput
|
||||
}
|
||||
}
|
||||
50
src/components/textBubble/bubbleDetails/drawer.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import { ref, onMounted } from "vue";
|
||||
import { Ref } from "@/types/vueRef/index";
|
||||
|
||||
/**
|
||||
* handles visualization of individual svg text bubbles
|
||||
*/
|
||||
export default function useBubbleDrawer({
|
||||
mr,
|
||||
tr,
|
||||
}: {
|
||||
mr: Ref<string>;
|
||||
tr: Ref<string>;
|
||||
}) {
|
||||
const signatureTranslate = ref("");
|
||||
let messageRect: SVGSVGElement & SVGRectElement;
|
||||
let messageText: SVGSVGElement & SVGTextElement;
|
||||
|
||||
onMounted(async () => {
|
||||
messageRect = (mr.value as unknown) as SVGSVGElement & SVGRectElement;
|
||||
messageText = (tr.value as unknown) as SVGSVGElement & SVGTextElement;
|
||||
});
|
||||
|
||||
function adjustMessageRect(): void {
|
||||
// must get dimensions of text box to fit bubble around
|
||||
const padding = 12.5;
|
||||
const textBox = messageText.getBBox();
|
||||
messageRect.setAttribute("x", String(textBox.x - padding));
|
||||
messageRect.setAttribute("y", String(textBox.y - padding));
|
||||
messageRect.setAttribute("width", String(textBox.width + 2 * padding));
|
||||
messageRect.setAttribute("height", String(textBox.height + 2 * padding));
|
||||
}
|
||||
|
||||
function translateMsgSignature(): void {
|
||||
if (messageRect.getAttribute("height") !== null) {
|
||||
const height = Number(messageRect.getAttribute("height"));
|
||||
signatureTranslate.value = `translate(-10 ${height})`;
|
||||
}
|
||||
}
|
||||
|
||||
async function drawMessage() {
|
||||
// ignore lint error: await needed for proper render
|
||||
await adjustMessageRect();
|
||||
translateMsgSignature();
|
||||
}
|
||||
|
||||
return {
|
||||
drawMessage,
|
||||
signatureTranslate,
|
||||
};
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
const { app } = require("electron");
|
||||
|
||||
const DOMAIN = "crimata.com";
|
||||
|
||||
/* If PROD env variable exists */
|
||||
const prod = process.env.PROD;
|
||||
|
||||
module.exports = {
|
||||
PLATFORM: prod ? `https://app.${DOMAIN}` : `http://localhost:5000`,
|
||||
API: prod ? `https://${DOMAIN}/api` : `http://localhost:5001`
|
||||
}
|
||||
69
src/io.js
|
|
@ -1,69 +0,0 @@
|
|||
const WebSocket = require("ws");
|
||||
const { ipcMain } = require("electron");
|
||||
|
||||
const config = require("./config");
|
||||
const { updateTray } = require("./tray");
|
||||
const { backgroundMitt, ipcEmit } = require("./utils/emitter");
|
||||
|
||||
let connection = null;
|
||||
|
||||
let pingId;
|
||||
let reconnectId;
|
||||
|
||||
function connectToPlatform(account)
|
||||
{
|
||||
if (pingId) clearInterval(pingId);
|
||||
|
||||
connection = new WebSocket(`${config.PLATFORM}/${account}`)
|
||||
|
||||
.on("open", () => pingId = setInterval(() => connection.ping(null, true), 1000))
|
||||
|
||||
.on("pong", () => setConnectionStatus(0))
|
||||
|
||||
.on("error", () => {}) /** keep silent on error */
|
||||
|
||||
.on("message", (payload) => backgroundMitt.emit("message", JSON.parse(payload)))
|
||||
|
||||
.on("close", (_code, reason) => {
|
||||
|
||||
setConnectionStatus(1);
|
||||
|
||||
if (reason)
|
||||
{
|
||||
if (reason == "unauthorized")
|
||||
{
|
||||
backgroundMitt.emit("logout", reason);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
reconnectId = setTimeout(() => connectToPlatform(account), 500);
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
function sendMessage(message)
|
||||
{
|
||||
if (connection.readyState === WebSocket.OPEN) {
|
||||
connection.send(JSON.stringify(message));
|
||||
} else console.log("Failed to send message");
|
||||
}
|
||||
|
||||
function disconnectFromPlatform()
|
||||
{
|
||||
clearTimeout(reconnectId);
|
||||
if (connection.open) connection.close(1000, "logout");
|
||||
}
|
||||
|
||||
function setConnectionStatus(status)
|
||||
{
|
||||
updateTray("disconnect", status);
|
||||
ipcEmit("ws", status);
|
||||
}
|
||||
|
||||
ipcMain.on("message", (_e, message) => sendMessage(message));
|
||||
|
||||
exports.connectToPlatform = connectToPlatform;
|
||||
exports.sendMessage = sendMessage;
|
||||
exports.disconnectFromPlatform = disconnectFromPlatform;
|
||||
28
src/main.js
|
|
@ -1,28 +0,0 @@
|
|||
const { app, protocol, ipcMain, nativeTheme } = require("electron");
|
||||
|
||||
const store = require("./utils/store");
|
||||
|
||||
const { initTray } = require("./tray");
|
||||
const { createWin } = require("./window");
|
||||
const { initAccount } = require("./account");
|
||||
// const { terminateAudio } = require("./audio");
|
||||
|
||||
// Assert a light theme
|
||||
nativeTheme.themeSource = "light";
|
||||
|
||||
app.on("ready", () =>
|
||||
{
|
||||
initTray();
|
||||
|
||||
createWin();
|
||||
|
||||
initAccount();
|
||||
});
|
||||
|
||||
// app.on("will-quit", terminateAudio);
|
||||
|
||||
// When user clicks app icon (re-open)
|
||||
app.on("activate", createWin);
|
||||
|
||||
// Prevents app from quitting on window close event
|
||||
app.on('window-all-closed', (e) => e.preventDefault());
|
||||
18
src/main.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
// src/main.ts
|
||||
|
||||
import App from "./App.vue";
|
||||
import router from "./router";
|
||||
|
||||
import mitt from "mitt";
|
||||
import anime from "animejs";
|
||||
import { createApp } from "vue";
|
||||
|
||||
// Handle events.
|
||||
const emitter = mitt();
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(router)
|
||||
app.provide("animejs", anime)
|
||||
app.provide("mitt", emitter)
|
||||
app.mount("#app");
|
||||
65
src/modules/auth.ts
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import { reactive, toRefs, ref } from 'vue';
|
||||
import { useIpc } from './ipc';
|
||||
|
||||
interface AuthState {
|
||||
accessToken: string | null;
|
||||
error?: Error;
|
||||
}
|
||||
|
||||
const state = reactive<AuthState>({
|
||||
accessToken: null,
|
||||
error: undefined,
|
||||
});
|
||||
|
||||
const loading = ref(true);
|
||||
|
||||
const AUTH_KEY = 'crimata_token';
|
||||
|
||||
const token = window.localStorage.getItem(AUTH_KEY);
|
||||
if (token) {
|
||||
state.accessToken = token;
|
||||
window.localStorage.setItem(AUTH_KEY, token);
|
||||
}
|
||||
|
||||
const authToken = async () => {
|
||||
loading.value = true;
|
||||
const { invoke } = useIpc();
|
||||
try {
|
||||
const res = await invoke('auth-session', token);
|
||||
window.localStorage.setItem(AUTH_KEY, res);
|
||||
state.accessToken = res;
|
||||
}
|
||||
catch(e) {
|
||||
state.error = e;
|
||||
console.log('ERROR: failed authentication');
|
||||
window.localStorage.removeItem(AUTH_KEY);
|
||||
state.accessToken = null;
|
||||
}
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
// authenticate on auth event
|
||||
window.ipcRenderer.on("auth", async (_event, _arg) => {
|
||||
await authToken();
|
||||
});
|
||||
|
||||
export const useAuth = () => {
|
||||
|
||||
const setToken = (token: string) => {
|
||||
window.localStorage.setItem(AUTH_KEY, token);
|
||||
state.accessToken = token;
|
||||
state.error = undefined;
|
||||
}
|
||||
|
||||
const logout = (): Promise<null> => {
|
||||
window.localStorage.removeItem(AUTH_KEY);
|
||||
return Promise.resolve(state.accessToken = null);
|
||||
}
|
||||
|
||||
return {
|
||||
setToken,
|
||||
logout,
|
||||
...toRefs(state), // accessToken, error
|
||||
loading
|
||||
}
|
||||
}
|
||||
23
src/modules/ipc.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
|
||||
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.postMessage({
|
||||
endpoint: endpoint,
|
||||
content: payload
|
||||
}, '*')
|
||||
};
|
||||
|
||||
return {
|
||||
invoke,
|
||||
post
|
||||
}
|
||||
}
|
||||
263
src/modules/keyDownHandler/keyBoardMaps/keyboardCharMap.ts
Normal file
|
|
@ -0,0 +1,263 @@
|
|||
// 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;
|
||||
261
src/modules/keyDownHandler/keyBoardMaps/keyboardNameMap.ts
Normal file
|
|
@ -0,0 +1,261 @@
|
|||
// 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;
|
||||
51
src/modules/keyDownHandler/useKeyDownHandler.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import { ref } from "vue";
|
||||
import keyboardNameMap from "./keyBoardMaps/keyboardNameMap";
|
||||
import keyboardCharMap from "./keyBoardMaps/keyboardCharMap";
|
||||
|
||||
interface KeyDownEvent {
|
||||
keyCode: number;
|
||||
shiftKey: boolean;
|
||||
}
|
||||
|
||||
// This catches keys from a physical keyboard, a soft keyboard on a tablet, or a
|
||||
// scanner attached via USB
|
||||
export default function useKeyDownHandler(enterCallback: (input: string) => void | null) {
|
||||
|
||||
const input = ref("");
|
||||
|
||||
function keyDownHandler(e: KeyDownEvent) {
|
||||
// keyboardCharMap is an array of arrays, with each inner
|
||||
// array having 2 columns - un-shifted, and shifted values.
|
||||
// iCol = 0 is the un-shifted value, while iCol=1 is the shifted value.
|
||||
// See the "keyboardCharMap", below, for printable characters.
|
||||
// MODIFY keyboardCharMap to suit your needs if you want
|
||||
// more/less/different characters to be considered "printable".
|
||||
let iCol = 0;
|
||||
if (e.shiftKey) {
|
||||
iCol = 1;
|
||||
}
|
||||
const ch = keyboardCharMap[e.keyCode][iCol];
|
||||
// Optionally do things with non-printables,
|
||||
// like CR, ESC, Backspace, LF, Tab, Arrows, F1-F24, etc.
|
||||
// See the arrary "keyboardNameMap", below, for possible keys.
|
||||
const cmd = keyboardNameMap[e.keyCode];
|
||||
switch (cmd) {
|
||||
case "ENTER":
|
||||
enterCallback(input.value);
|
||||
break;
|
||||
case "BACK_SPACE":
|
||||
input.value = input.value.slice(0, -1);
|
||||
break;
|
||||
case "ESCAPE":
|
||||
input.value = "";
|
||||
break;
|
||||
default:
|
||||
input.value += ch;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
keyDownHandler,
|
||||
input
|
||||
};
|
||||
}
|
||||
13
src/modules/mediaStream.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
export default async function useMediaStream(constraints: {
|
||||
audio: boolean;
|
||||
video?: boolean;
|
||||
}): Promise<MediaStream> {
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia(constraints);
|
||||
return stream;
|
||||
} catch (e) {
|
||||
return e;
|
||||
}
|
||||
// var audioTack = stream.getAudioTracks()[0];
|
||||
// console.log(audioTrack.getSettings())
|
||||
}
|
||||
17
src/modules/mitt.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { inject } from "vue";
|
||||
import { Mitt } from "@/types/mitt/index";
|
||||
|
||||
let emitter: Mitt;
|
||||
|
||||
export default function useMitt() {
|
||||
|
||||
const emitterInject: Mitt | undefined = inject("mitt");
|
||||
if (emitterInject) {
|
||||
emitter = emitterInject;
|
||||
}
|
||||
return {
|
||||
emitter
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
78
src/modules/queue.ts
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
export interface DataNode<T> {
|
||||
data: T;
|
||||
next: DataNode<T> | null;
|
||||
}
|
||||
|
||||
export interface QueueI<T> {
|
||||
enqueue(data: T): void;
|
||||
dequeue(): T | null;
|
||||
peek(): T | null;
|
||||
size(): number;
|
||||
}
|
||||
|
||||
export class Queue<T> implements QueueI<T> {
|
||||
private _size: number;
|
||||
private head: DataNode<T> | null;
|
||||
private tail: DataNode<T> | null;
|
||||
constructor() {
|
||||
this._size = 0;
|
||||
this.head = this.tail = null;
|
||||
}
|
||||
|
||||
enqueue(data: T) {
|
||||
|
||||
const node: DataNode<T> = {
|
||||
data,
|
||||
next: null
|
||||
}
|
||||
if (this.head === null) {
|
||||
this.head = this.tail = node;
|
||||
} else {
|
||||
if (this.tail) {
|
||||
this.tail.next = node;
|
||||
this.tail = this.tail.next;
|
||||
}
|
||||
|
||||
}
|
||||
++this._size;
|
||||
}
|
||||
|
||||
dequeue(): T | null {
|
||||
|
||||
let item: T | null = null;
|
||||
|
||||
if (this._size === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this.head) {
|
||||
item = this.head.data;
|
||||
this.head = this.head.next;
|
||||
--this._size;
|
||||
|
||||
if (!this._size) {
|
||||
this.tail = null;
|
||||
}
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
peek(): T | null {
|
||||
|
||||
let item: T | null = null;
|
||||
|
||||
if (this._size === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this.head) {
|
||||
item = this.head.data;
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
size(): number {
|
||||
return this._size;
|
||||
}
|
||||
}
|
||||
86
src/modules/scrollView.ts
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
import { ref, computed, onMounted, onUnmounted } from "vue";
|
||||
|
||||
/**
|
||||
* handles visualization of view messages through
|
||||
* vue transition callbacks and anime-js animations
|
||||
*/
|
||||
export default function useScrollView({ targetId }: { targetId: string }) {
|
||||
|
||||
const scroll = ref(0);
|
||||
let scrollTarget: HTMLElement | SVGElement | null;
|
||||
const targetHeight = ref(0);
|
||||
let topBound: number;
|
||||
const scrollStartHeight = 445;
|
||||
|
||||
const scrollView = computed(() => {
|
||||
|
||||
if (clampedScroll.value === 0) {
|
||||
scroll.value = 0;
|
||||
} else if (clampedScroll.value === topBound) {
|
||||
scroll.value = topBound;
|
||||
}
|
||||
|
||||
// x, y, width, height
|
||||
// width and height should be dynamic
|
||||
return `0 ${clampedScroll.value} 350 500`;
|
||||
});
|
||||
|
||||
const clampedScroll = computed(() => {
|
||||
if (targetHeight.value === 0) { return 0; }
|
||||
topBound = -targetHeight.value + 450;
|
||||
return Math.max(topBound, Math.min(scroll.value, 0)) as number;
|
||||
})
|
||||
|
||||
const getHeight = () => {
|
||||
if (scrollTarget) {
|
||||
return scrollTarget.getBoundingClientRect().height as number;
|
||||
}
|
||||
}
|
||||
|
||||
const getWidth = () => {
|
||||
if (scrollTarget) {
|
||||
return scrollTarget.getBoundingClientRect().width as number;
|
||||
}
|
||||
}
|
||||
|
||||
const updateView = (verticalScroll: number) => {
|
||||
|
||||
if (scrollTarget) {
|
||||
|
||||
const heightResult = getHeight();
|
||||
const widthResult = getWidth();
|
||||
|
||||
console.log(heightResult);
|
||||
console.log(widthResult);
|
||||
|
||||
if (heightResult) {
|
||||
// only scroll if renderer scrollStart
|
||||
if (heightResult > scrollStartHeight) {
|
||||
targetHeight.value = heightResult;
|
||||
scroll.value += verticalScroll;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const scrollUpdate = (e: any): void => {
|
||||
const verticalScroll = e.deltaY * 0.25;
|
||||
updateView(verticalScroll);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
scrollTarget = document.getElementById("targetId");
|
||||
if (scrollTarget) {
|
||||
window.addEventListener("wheel", scrollUpdate);
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener("wheel", scrollUpdate);
|
||||
})
|
||||
|
||||
return {
|
||||
scrollView
|
||||
}
|
||||
|
||||
}
|
||||
17
src/modules/textWrap.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
export default function useTextWrap() {
|
||||
// text wraps a string to a given width
|
||||
function textWrap({ s, w }: { s: string; w: number }): string[] {
|
||||
// return tspan elements for text with correct size
|
||||
const wrap = (s: string, w: number) =>
|
||||
s.replace(
|
||||
new RegExp(`(?![^\\n]{1,${w}}$)([^\\n]{1,${w}})\\s`, "g"),
|
||||
"$1\n"
|
||||
);
|
||||
const result = wrap(s, w).split("\n");
|
||||
return result;
|
||||
}
|
||||
|
||||
return {
|
||||
textWrap
|
||||
};
|
||||
}
|
||||
34
src/preload.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
// All of the Node.js APIs are available in the preload process.
|
||||
// It has the same sandbox as a Chrome extension.
|
||||
import { ipcRenderer } from "electron";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
ipcRenderer: typeof ipcRenderer;
|
||||
}
|
||||
}
|
||||
|
||||
window.ipcRenderer = ipcRenderer;
|
||||
|
||||
process.once("loaded", () => {
|
||||
|
||||
window.addEventListener("message", event => {
|
||||
|
||||
// do something with custom event
|
||||
const message = event.data;
|
||||
|
||||
if (message.endpoint === "update-menu-bar") {
|
||||
ipcRenderer.send("update-menu-bar", message.content);
|
||||
}
|
||||
|
||||
if (message.endpoint === "update-recorder") {
|
||||
ipcRenderer.send("update-recorder", message.content);
|
||||
}
|
||||
|
||||
if (message.endpoint === "message") {
|
||||
ipcRenderer.send("message", message.content);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
<svg id="logo" xmlns="http://www.w3.org/2000/svg" width="24.87" height="23.001" viewBox="0 0 24.87 23.001">
|
||||
<g id="Group_33" data-name="Group 33">
|
||||
<g id="Group_32" data-name="Group 32" transform="translate(0 0)">
|
||||
<g id="Group_31" data-name="Group 31" transform="translate(0 11.948)">
|
||||
<circle id="Ellipse_50" data-name="Ellipse 50" cx="5.527" cy="5.527" r="5.527" transform="translate(0 0)" fill="#383838"/>
|
||||
<circle id="Ellipse_51" data-name="Ellipse 51" cx="5.527" cy="5.527" r="5.527" transform="translate(13.817 0)" fill="#383838"/>
|
||||
</g>
|
||||
<circle id="Ellipse_52" data-name="Ellipse 52" cx="5.527" cy="5.527" r="5.527" transform="translate(6.898)" fill="#383838"/>
|
||||
<path id="Path_150" data-name="Path 150" d="M36.27,83.67" transform="translate(-30.733 -66.196)" fill="#ff0"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 845 B |
|
|
@ -1,7 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="25" height="5" viewBox="0 0 25 5">
|
||||
<g id="Group_611" data-name="Group 611" transform="translate(-4032 499)">
|
||||
<circle id="Ellipse_71" data-name="Ellipse 71" cx="2.5" cy="2.5" r="2.5" transform="translate(4032 -499)" fill="#9b9b9b"/>
|
||||
<circle id="Ellipse_72" data-name="Ellipse 72" cx="2.5" cy="2.5" r="2.5" transform="translate(4042 -499)" fill="#9b9b9b"/>
|
||||
<circle id="Ellipse_73" data-name="Ellipse 73" cx="2.5" cy="2.5" r="2.5" transform="translate(4052 -499)" fill="#9b9b9b"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 553 B |
|
|
@ -1,44 +0,0 @@
|
|||
import Login from "./login.js";
|
||||
import Splash from "./splash.js";
|
||||
import Header from "./header.js";
|
||||
import Messenger from "./messenger.js";
|
||||
|
||||
const account = Vue.ref(null);
|
||||
|
||||
const App =
|
||||
{
|
||||
components: {
|
||||
Splash,
|
||||
Header,
|
||||
Messenger,
|
||||
Login
|
||||
},
|
||||
|
||||
setup()
|
||||
{
|
||||
return { account };
|
||||
},
|
||||
|
||||
template: `
|
||||
<div id="app" v-if="account !== null">
|
||||
|
||||
<Header />
|
||||
|
||||
<Messenger v-if="account"/>
|
||||
<Login v-else />
|
||||
|
||||
</div>
|
||||
|
||||
<Splash v-else />`
|
||||
}
|
||||
|
||||
window.mainApi.on("account", (value, reason) => {
|
||||
if (reason) alert(reason);
|
||||
account.value = value;
|
||||
});
|
||||
|
||||
export default App;
|
||||
export { account };
|
||||
|
||||
// Is called when window is about to close or reload
|
||||
window.onbeforeunload = () => console.log("beforeunload");
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
const Bubble =
|
||||
{
|
||||
props: ["category", "text", "html", "blob", "modifier", "child"],
|
||||
|
||||
setup(props)
|
||||
{
|
||||
const getUrl = () => {
|
||||
return `data:${props.category};base64,${props.blob}`;
|
||||
}
|
||||
|
||||
Vue.onMounted(() => {
|
||||
|
||||
const el = document.getElementById("messenger");
|
||||
|
||||
setTimeout(() => {
|
||||
|
||||
el.dispatchEvent(new CustomEvent('adjust-scroll'));
|
||||
|
||||
}, 100);
|
||||
|
||||
});
|
||||
|
||||
if (!props.text) props.text = "...";
|
||||
|
||||
return { getUrl };
|
||||
},
|
||||
|
||||
template: `
|
||||
<div :class="'bubble ' + modifier + '-bubble ' + modifier +'-'+ child">
|
||||
|
||||
<small class="bubble-notify" v-if="modifier=='ai'"></small>
|
||||
|
||||
<p v-if="category=='text'">{{ text }}</p>
|
||||
|
||||
<p v-else-if="category=='audio'">{{ text }}</p>
|
||||
|
||||
<div v-else-if="category=='html'" v-html="html"></div>
|
||||
|
||||
<img v-else-if="category == 'image'" :src="getUrl()" />
|
||||
|
||||
<a v-else :download="text" :href="getUrl()">{{ text }}</a>
|
||||
|
||||
</div>`
|
||||
};
|
||||
|
||||
export default Bubble;
|
||||
|
||||
// TODO: should include other styling/actions for audio bubbles
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
|
||||
const Context =
|
||||
{
|
||||
props: ["modifier", "context", "avatar", "id"],
|
||||
|
||||
setup(props) {
|
||||
const playback = Vue.ref(false);
|
||||
|
||||
Vue.onMounted(() => {
|
||||
window.mainApi.on("playback", (id, status) => {
|
||||
if (id === props.id) {
|
||||
playback.value = status;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return { playback };
|
||||
},
|
||||
|
||||
template: `
|
||||
<span :class="'context ' + modifier + '-context'">
|
||||
|
||||
<img v-show="avatar" class="context-avatar"
|
||||
:class="{ audio: playback }"
|
||||
:src="'data:image/png;base64,' + avatar"
|
||||
/>
|
||||
|
||||
{{ context }}
|
||||
|
||||
</span> `
|
||||
}
|
||||
|
||||
export default Context;
|
||||
|
|
@ -1,195 +0,0 @@
|
|||
const saveLocation = "input_item_position";
|
||||
const defaultPosition = { x: 15, y: 400 };
|
||||
|
||||
//---Dragabble Helper Funcs--------------------------------------
|
||||
|
||||
// Calculate distance to nearest side.
|
||||
function calcSideProximity (elementX, elementLength, winW) {
|
||||
|
||||
// Calc right short.
|
||||
let short = winW - elementX - elementLength;
|
||||
|
||||
// See if it's left short.
|
||||
if (elementX + 20 < winW / 2) {
|
||||
short = elementX
|
||||
}
|
||||
|
||||
return short
|
||||
}
|
||||
|
||||
// Update elementX or elementY value on window resize
|
||||
function calcPosition (elementPosition, elementLength, percent, short, win) {
|
||||
|
||||
// Is it close to the right/bottom side?
|
||||
if (percent > 0.75) {
|
||||
elementPosition = win - short - elementLength;
|
||||
}
|
||||
|
||||
// Is it not close to a side?
|
||||
if (percent < 0.75 && percent > 0.25) {
|
||||
elementPosition = win * percent
|
||||
}
|
||||
|
||||
return elementPosition
|
||||
}
|
||||
|
||||
function draggify(elementId, parentId, margin) {
|
||||
|
||||
let element;
|
||||
let parent;
|
||||
|
||||
/* only compatible with elements having equal width and height */
|
||||
let elementLength;
|
||||
|
||||
// Cords of inputItem.
|
||||
const elementX = Vue.ref();
|
||||
const elementY = Vue.ref();
|
||||
|
||||
// Position of inputItem on terms of percentage of window.
|
||||
let percentX;
|
||||
let percentY;
|
||||
|
||||
// How close inputItem is to closest X or Y side.
|
||||
let xShort;
|
||||
let yShort;
|
||||
|
||||
//---Reposition Anime-----------------------------------------------
|
||||
|
||||
// Move element to target smoothly.
|
||||
const repositionAnime = (xChange, yChange) => {
|
||||
|
||||
const xStep = xChange / 1000;
|
||||
const yStep = yChange / 1000;
|
||||
|
||||
for (let i = 1; i <= 1000; i++) {
|
||||
|
||||
setTimeout(() => {
|
||||
elementX.value += xStep;
|
||||
elementY.value += yStep;
|
||||
}, 30) // 60fps
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
//---Event Handlers-----------------------------------------------
|
||||
|
||||
// Update the position of inputItem on mouse dragging.
|
||||
const onMouseMove = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
elementX.value = element.offsetLeft + e.movementX;
|
||||
elementY.value = element.offsetTop + e.movementY;
|
||||
}
|
||||
|
||||
// Add an event listener for dragging.
|
||||
const onMouseDown = (e) => {
|
||||
e.preventDefault();
|
||||
window.addEventListener('mousemove', onMouseMove);
|
||||
window.addEventListener('mouseup', onMouseUp);
|
||||
}
|
||||
|
||||
// Update position references when user is done moving targetEl.
|
||||
const onMouseUp = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
window.removeEventListener('mousemove', onMouseMove);
|
||||
window.removeEventListener('mouseup', onMouseUp);
|
||||
|
||||
// See if and calculate reposition.
|
||||
let x = 0; // vector change
|
||||
let y = 0;
|
||||
|
||||
const winW = parent.clientWidth;
|
||||
const winH = parent.clientHeight;
|
||||
|
||||
if (elementX.value < 0) {
|
||||
x = (elementX.value - margin)*-1
|
||||
}
|
||||
|
||||
if (elementX.value > winW - elementLength) {
|
||||
const b = winW - elementLength - margin;
|
||||
x = (elementX.value - b)*-1
|
||||
}
|
||||
|
||||
// Reposition y
|
||||
if (elementY.value < 0) {
|
||||
y = (elementY.value - margin)*-1
|
||||
}
|
||||
|
||||
if (elementY.value > winH - elementLength) {
|
||||
const d = winH - elementLength - margin;
|
||||
y = (elementY.value - d)*-1
|
||||
}
|
||||
|
||||
// Reposition if needed.
|
||||
if (x !== 0 || y !== 0) repositionAnime(x, y);
|
||||
|
||||
xShort = calcSideProximity(elementX.value, elementLength, winW);
|
||||
yShort = calcSideProximity(elementY.value, elementLength, winH);
|
||||
|
||||
// Update percentages.
|
||||
percentX = elementX.value / winW;
|
||||
percentY = elementY.value / winH;
|
||||
|
||||
// Save position.
|
||||
savePosition();
|
||||
}
|
||||
|
||||
// Update position of targetEl on windowResize.
|
||||
const onWindowResize = (_e) => {
|
||||
|
||||
elementX.value = calcPosition(elementX.value, elementLength, percentX, xShort, parent.clientWidth);
|
||||
elementY.value = calcPosition(elementY.value, elementLength, percentY, yShort, parent.clientHeight);
|
||||
|
||||
savePosition();
|
||||
}
|
||||
|
||||
const savePosition = () => {
|
||||
window.localStorage.setItem(saveLocation, JSON.stringify({
|
||||
x: elementX.value,
|
||||
y: elementY.value
|
||||
}));
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------
|
||||
|
||||
Vue.onMounted(() => {
|
||||
|
||||
element = document.getElementById(elementId);
|
||||
parent = document.getElementById(parentId);
|
||||
|
||||
elementLength = element.offsetWidth;
|
||||
|
||||
// Initialize the positional references.
|
||||
percentX = elementX.value / parent.clientWidth;
|
||||
percentY = elementY.value / parent.clientHeight;
|
||||
|
||||
xShort = calcSideProximity(elementX.value, elementLength, parent.clientWidth);
|
||||
yShort = calcSideProximity(elementX.value, elementLength, parent.clientHeight);
|
||||
|
||||
// Then, we can listen for window resize (and mousedown).
|
||||
element.addEventListener("mousedown", onMouseDown);
|
||||
parent.addEventListener('resize', onWindowResize)
|
||||
});
|
||||
|
||||
// remove event listeners on component dismount.
|
||||
Vue.onUnmounted(() => {
|
||||
element.removeEventListener('mousedown', onMouseDown);
|
||||
parent.removeEventListener('resize', onWindowResize)
|
||||
parent.removeEventListener('mouseup', onMouseUp)
|
||||
parent.removeEventListener('mousemove', onMouseMove);
|
||||
});
|
||||
|
||||
// Try loading initPosition, otherwise set default values
|
||||
let initPosition;
|
||||
const rawData = window.localStorage.getItem("saveLocation")
|
||||
rawData ? initPosition = JSON.parse(rawData) : initPosition = defaultPosition;
|
||||
|
||||
elementX.value = initPosition.x;
|
||||
elementY.value = initPosition.y;
|
||||
|
||||
return { elementX, elementY };
|
||||
|
||||
}
|
||||
|
||||
export default draggify;
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
const onDrop = async (e) => {
|
||||
const file = e.dataTransfer.items[0].getAsFile();
|
||||
|
||||
if (file.size >= 1 * 1000 * 1000) {
|
||||
alert("File must be under 1MB.");
|
||||
return;
|
||||
}
|
||||
|
||||
const buffer = await file.arrayBuffer();
|
||||
const b64String = await window.mainApi.invoke("encode", buffer);
|
||||
|
||||
let category = "file";
|
||||
if (file.type.startsWith("image/")) {
|
||||
category = "image"
|
||||
}
|
||||
|
||||
window.mainApi.send("message", {
|
||||
category: category,
|
||||
text: file.name,
|
||||
blob: b64String
|
||||
});
|
||||
}
|
||||
|
||||
export default onDrop;
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
let el;
|
||||
|
||||
const scroll = () => {
|
||||
if (!el) {
|
||||
el = document.getElementById("messenger");
|
||||
window.addEventListener('resize', scroll);
|
||||
}
|
||||
el.scrollTo({
|
||||
top: el.scrollHeight - el.clientHeight,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
|
||||
export default scroll;
|
||||
|
||||
// const isBottom = () => {
|
||||
// if (el) {
|
||||
// return el.scrollHeight - el.clientHeight <= el.scrollTop + 1;
|
||||
// }
|
||||
// }
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
const inputLength = 230;
|
||||
|
||||
const metaKeys = [
|
||||
"Tab",
|
||||
"CapsLock",
|
||||
"Shift",
|
||||
"Control",
|
||||
"Alt",
|
||||
"Meta",
|
||||
" ",
|
||||
"ArrowLeft",
|
||||
"ArrowRight",
|
||||
"ArrowUp",
|
||||
"ArrowDown",
|
||||
"Enter",
|
||||
"Backspace",
|
||||
"Escape"
|
||||
];
|
||||
|
||||
function useText(elementId, parentId, left)
|
||||
{
|
||||
let el;
|
||||
let parent;
|
||||
|
||||
const show = Vue.ref(false);
|
||||
const leftSide = Vue.ref(false);
|
||||
|
||||
const calcSide = () => {
|
||||
parent.clientWidth - left.value < inputLength ? leftSide.value = true : leftSide.value = false;
|
||||
}
|
||||
|
||||
/* handle user typing */
|
||||
Vue.onMounted(() => {
|
||||
|
||||
el = document.getElementById(elementId);
|
||||
parent = document.getElementById(parentId);
|
||||
|
||||
window.addEventListener("keydown", (e) => {
|
||||
|
||||
if (!show.value && !metaKeys.includes(e.key) && !e.ctrlKey)
|
||||
{
|
||||
show.value = true;
|
||||
}
|
||||
else if (show.value && ((el.value.length === 1 && e.key === "Backspace") || e.key === "Escape"))
|
||||
{
|
||||
show.value = false;
|
||||
}
|
||||
|
||||
if (e.key === "Enter" && el.value)
|
||||
{
|
||||
if (el.value.length >= 250) {
|
||||
alert("250 character limit")
|
||||
return;
|
||||
}
|
||||
|
||||
window.mainApi.send("message", {
|
||||
category: "text",
|
||||
text: el.value,
|
||||
blob: null
|
||||
});
|
||||
|
||||
show.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
calcSide();
|
||||
|
||||
});
|
||||
|
||||
Vue.watch(left, calcSide);
|
||||
|
||||
Vue.watch(show, (c, _p) => {
|
||||
if (c) {
|
||||
el.focus();
|
||||
} else {
|
||||
el.value = "";
|
||||
el.blur();
|
||||
}
|
||||
});
|
||||
|
||||
return { leftSide, show };
|
||||
}
|
||||
|
||||
export default useText;
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
export const isEmail = (value) => {
|
||||
console.log(value);
|
||||
if (!/(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/.test(value)) {
|
||||
return "Not a valid email."
|
||||
}
|
||||
}
|
||||
|
||||
export const isPassword = (value) => {
|
||||
if (!/^(?=.*[0-9])(?=.*[!@#$%^&*])[a-zA-Z0-9!@#$%^&*]{6,16}$/.test(value)) {
|
||||
return "Password must have symbol, number, uppercase and be 6-16 length.";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Name is optional and therefore doesn't return error if no value.
|
||||
*/
|
||||
export const isName = (value) => {
|
||||
if (value) {
|
||||
if (!/^([a-zA-Z ]){5,30}$/.test(value)) {
|
||||
return "Name must be at least 5 long.";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
|
||||
// Basic debounce implentation
|
||||
const debounce = (fn, delay) => {
|
||||
let timeoutId;
|
||||
return (...args) => {
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
timeoutId = setTimeout(() => {
|
||||
fn(...args);
|
||||
}, delay);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamic form component with auto submission and validation.
|
||||
*
|
||||
* props
|
||||
* .fields => List of field objects
|
||||
* .channel => e.g. "/login"
|
||||
* .modifier => Form description
|
||||
*/
|
||||
const SmartForm =
|
||||
{
|
||||
props: ["fields", "channel", "modifier"],
|
||||
|
||||
setup(props)
|
||||
{
|
||||
// Error log below form
|
||||
const log = Vue.ref(props.modifier);
|
||||
|
||||
/**
|
||||
* Valid and submit form data to channel.
|
||||
*/
|
||||
const submit = async () => {
|
||||
|
||||
// Validate each field
|
||||
for (let field of props.fields) {
|
||||
const error = field.valid(field.value);
|
||||
if (error) return error;
|
||||
}
|
||||
|
||||
// Reduce fields into the form
|
||||
const form = props.fields.reduce((v, n) => {
|
||||
v[n.name] = n.value;
|
||||
return v;
|
||||
}, {});
|
||||
|
||||
console.log("Submitting =>", form);
|
||||
return await window.mainApi.invoke(props.channel, form);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for submit that is debounced and updates UI elements
|
||||
*/
|
||||
const preSub = debounce(async () => {
|
||||
|
||||
const error = await submit();
|
||||
|
||||
log.value = error ? error : props.modifier
|
||||
|
||||
}, 2000);
|
||||
|
||||
return { preSub, log };
|
||||
},
|
||||
|
||||
template: `
|
||||
<form id="smart-form" class="smart-form" v-on:input="preSub">
|
||||
|
||||
<div v-for="field in fields" style="display:flex">
|
||||
|
||||
<input type="text" class="text-field"
|
||||
v-model="field.value"
|
||||
:name="field.name"
|
||||
:placeholder="field.placeholder"
|
||||
/>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="smart-form-logs">{{ log }}</div>
|
||||
|
||||
</form>`
|
||||
}
|
||||
|
||||
export default SmartForm;
|
||||
|
||||
|
||||
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
const Header =
|
||||
{
|
||||
setup() {
|
||||
const onNavBar = (command) => window.mainApi.send("nav", command);
|
||||
return { onNavBar };
|
||||
},
|
||||
|
||||
template: `
|
||||
<button id="header-titlebar" />
|
||||
|
||||
<span id="header-menu">
|
||||
<button
|
||||
class="header-menu-button header-exit-button button"
|
||||
@click.prevent="onNavBar('close')"
|
||||
/>
|
||||
<button
|
||||
class="header-menu-button header-min-button button"
|
||||
@click.prevent="onNavBar('min')"
|
||||
/>
|
||||
</span> `
|
||||
}
|
||||
|
||||
export default Header;
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
import useText from "./control/text.js";
|
||||
import draggify from "./control/draggify.js";
|
||||
|
||||
const Input =
|
||||
{
|
||||
props: ["person"],
|
||||
|
||||
setup(props)
|
||||
{
|
||||
const initials = Vue.ref("");
|
||||
|
||||
const recording = Vue.ref(false);
|
||||
|
||||
const { elementX, elementY } = draggify("input", "app", 15);
|
||||
|
||||
const { leftSide, show } = useText("input-text", "app", elementX);
|
||||
|
||||
window.mainApi.on("record", (status) => recording.value = status);
|
||||
|
||||
Vue.watch(() => props.person, (c, _p) => initials.value = getInitials(c));
|
||||
|
||||
return { initials, elementX, elementY, recording, leftSide, show };
|
||||
},
|
||||
|
||||
template: `
|
||||
<div id="input" :class="{ audio: recording }"
|
||||
:style="{ top: elementY + 'px', left: elementX + 'px' }"
|
||||
>
|
||||
|
||||
<div id="input-icon">{{ initials }}</div>
|
||||
|
||||
<input
|
||||
id="input-text" type="text"
|
||||
:class="[
|
||||
{ leftSide: leftSide },
|
||||
{ show: show },
|
||||
{ recording: recording }
|
||||
]"
|
||||
/>
|
||||
|
||||
</div> `
|
||||
}
|
||||
|
||||
function getInitials(name)
|
||||
{
|
||||
let rgx = new RegExp(/(\p{L}{1})\p{L}+/, 'gu');
|
||||
|
||||
let initials = [...name.matchAll(rgx)] || [];
|
||||
|
||||
return (
|
||||
(initials.shift()?.[1] || '') + (initials.pop()?.[1] || '')
|
||||
).toUpperCase();
|
||||
}
|
||||
|
||||
export default Input;
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
import SmartForm from "./form.js";
|
||||
import { isEmail, isPassword, isName } from "./control/valid.js";
|
||||
|
||||
const Login =
|
||||
{
|
||||
components: {
|
||||
SmartForm
|
||||
},
|
||||
|
||||
setup()
|
||||
{
|
||||
return { isEmail, isPassword, isName };
|
||||
},
|
||||
|
||||
template: `
|
||||
<div id="login">
|
||||
|
||||
<SmartForm
|
||||
:fields="[{
|
||||
name: 'email',
|
||||
value: null,
|
||||
placeholder: 'Email',
|
||||
valid: isEmail
|
||||
},
|
||||
{
|
||||
name: 'password',
|
||||
value: null,
|
||||
placeholder: 'Password',
|
||||
valid: isPassword
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
value: null,
|
||||
placeholder: 'Name',
|
||||
valid: isName
|
||||
}]"
|
||||
:channel="'auth'"
|
||||
:modifier="'Login or enter name to register.'"
|
||||
/>
|
||||
|
||||
</div>`
|
||||
}
|
||||
|
||||
export default Login;
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
import Bubble from "./bubble.js";
|
||||
import Context from "./context.js";
|
||||
|
||||
const Message =
|
||||
{
|
||||
components: {
|
||||
Bubble,
|
||||
Context
|
||||
},
|
||||
|
||||
props: ["modifier", "content", "context", "avatar", "id", "seen"],
|
||||
|
||||
setup(props)
|
||||
{
|
||||
return { calcChild };
|
||||
},
|
||||
|
||||
template: `
|
||||
<div :id="id" :class="'message ' + modifier + '-message'">
|
||||
|
||||
<Bubble
|
||||
v-for="(c, index) in content"
|
||||
:id="'bubble-' + id + '-' + index"
|
||||
:category="c.category"
|
||||
:text="c.text"
|
||||
:html="c.html"
|
||||
:blob="c.blob"
|
||||
:modifier="modifier"
|
||||
:child="calcChild(index, content.length)"
|
||||
/>
|
||||
|
||||
<Context
|
||||
:modifier="modifier"
|
||||
:context="context"
|
||||
:avatar="avatar"
|
||||
:id="id"
|
||||
/>
|
||||
|
||||
</div> `
|
||||
}
|
||||
|
||||
function calcChild(index, len)
|
||||
{
|
||||
if (len === 1)
|
||||
{
|
||||
return "none-child";
|
||||
}
|
||||
else if (index === 0)
|
||||
{
|
||||
return "first-child";
|
||||
}
|
||||
else if (index === len - 1)
|
||||
{
|
||||
return "last-child";
|
||||
}
|
||||
else
|
||||
{
|
||||
return "middle-child";
|
||||
}
|
||||
}
|
||||
|
||||
export default Message;
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
import onDrop from "./control/drop.js";
|
||||
import scroll from "./control/scroll.js";
|
||||
|
||||
import WS from "./ws.js";
|
||||
import Input from "./input.js";
|
||||
import Settings from "./settings.js";
|
||||
import Message from "./message.js";
|
||||
|
||||
const Messenger = {
|
||||
|
||||
components: {
|
||||
WS,
|
||||
Settings,
|
||||
Message,
|
||||
Input
|
||||
},
|
||||
|
||||
setup()
|
||||
{
|
||||
const person = Vue.ref("");
|
||||
const messages = Vue.ref([]);
|
||||
|
||||
let current = Vue.ref();
|
||||
|
||||
Vue.onMounted(() => {
|
||||
|
||||
window.mainApi.on("message", (update) => {
|
||||
|
||||
if (update.person)
|
||||
{
|
||||
person.value = update.person;
|
||||
messages.value = update.messages;
|
||||
current.value = messages.value.at(-1);
|
||||
}
|
||||
else if (update.name)
|
||||
{
|
||||
person.value = update.name;
|
||||
}
|
||||
else if (update.modifier)
|
||||
{
|
||||
messages.value.push(update);
|
||||
current.value = update;
|
||||
}
|
||||
else if (update.category)
|
||||
{
|
||||
current.value.content.push(update);
|
||||
}
|
||||
else if (update.context)
|
||||
{
|
||||
current.value.context = update.context;
|
||||
}
|
||||
else
|
||||
{
|
||||
current.value.content.at(-1).text = update.text;
|
||||
}
|
||||
|
||||
setTimeout(scroll, 10);
|
||||
|
||||
});
|
||||
|
||||
window.mainApi.send("messenger");
|
||||
});
|
||||
|
||||
return { person, messages, onDrop };
|
||||
},
|
||||
|
||||
template: `
|
||||
<Settings />
|
||||
<WS />
|
||||
<Input :person="person" />
|
||||
|
||||
<div id="messenger"
|
||||
@drop="onDrop($event)"
|
||||
@dragover.prevent
|
||||
@dragenter.prevent
|
||||
>
|
||||
|
||||
<Message
|
||||
v-for="message in messages"
|
||||
v-bind="message"
|
||||
/>
|
||||
|
||||
</div>`
|
||||
}
|
||||
|
||||
export default Messenger;
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
|
||||
const Settings =
|
||||
{
|
||||
setup() {
|
||||
const active = Vue.ref(false);
|
||||
|
||||
function hideSettings(e) {
|
||||
if (e.key == "Escape") {
|
||||
active.value = false;
|
||||
window.removeEventListener("keydown", hideSettings);
|
||||
}
|
||||
}
|
||||
|
||||
function showSettings() {
|
||||
active.value = true;
|
||||
window.addEventListener("keydown", hideSettings);
|
||||
}
|
||||
|
||||
const logout = async () => {
|
||||
await window.mainApi.send("logout");
|
||||
}
|
||||
|
||||
return { showSettings, active, logout, window };
|
||||
},
|
||||
|
||||
template: `
|
||||
<div v-if="active" id="settings">
|
||||
|
||||
<button class="settings-logout-button button" @click="logout">
|
||||
Logout
|
||||
</button>
|
||||
|
||||
</div>
|
||||
|
||||
<button v-else class="settings-icon" @click="showSettings">
|
||||
<img :src="window.path + 'assets/settingsIcon.svg'">
|
||||
</button>`
|
||||
}
|
||||
|
||||
export default Settings;
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
const Splash =
|
||||
{
|
||||
template: `
|
||||
<div id="splash">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="57.046"
|
||||
height="52.759"
|
||||
viewBox="0 0 57.046 52.759"
|
||||
>
|
||||
<g transform="translate(-4127 -507)">
|
||||
<g transform="translate(3949 332.206)">
|
||||
<g transform="translate(178 174.794)">
|
||||
<g transform="translate(0 27.405)">
|
||||
<circle
|
||||
cx="12.677"
|
||||
cy="12.677"
|
||||
r="12.677"
|
||||
transform="translate(0 0)"
|
||||
fill="#383838"
|
||||
/>
|
||||
<circle
|
||||
cx="12.677"
|
||||
cy="12.677"
|
||||
r="12.677"
|
||||
transform="translate(31.692 0)"
|
||||
fill="#383838"
|
||||
/>
|
||||
</g>
|
||||
<circle
|
||||
cx="12.677"
|
||||
cy="12.677"
|
||||
r="12.677"
|
||||
transform="translate(15.822 0)"
|
||||
fill="#383838"
|
||||
/>
|
||||
<path
|
||||
d="M36.27,83.67"
|
||||
transform="translate(-23.569 -43.588)"
|
||||
fill="#ff0"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
export default Splash;
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
|
||||
const WS =
|
||||
{
|
||||
setup()
|
||||
{
|
||||
const status = Vue.ref();
|
||||
|
||||
Vue.onMounted(() => {
|
||||
|
||||
window.mainApi.on("ws", (val) => {
|
||||
status.value = val;
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
return { status, window }
|
||||
},
|
||||
|
||||
template: `
|
||||
<div id="ws">
|
||||
|
||||
<img id="ws-logo" :src="window.path + 'assets/connectLogo.svg'"
|
||||
:class="{ move: status }"
|
||||
>
|
||||
|
||||
<div id="ws-dots"
|
||||
:class="{ move: status }"
|
||||
>
|
||||
<div class="ws-dot"></div>
|
||||
</div>
|
||||
|
||||
</div> `
|
||||
}
|
||||
|
||||
export default WS;
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="stylesheet" href="./main.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="app"></div>
|
||||
|
||||
<!-- [email protected] -->
|
||||
<script src="./vendor/vue.js"></script>
|
||||
|
||||
<script type="module" src="./index.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
import App from "./components/app.js";
|
||||
|
||||
window.path = "./";
|
||||
|
||||
Vue.createApp(App).mount("#app");
|
||||
|
|
@ -1,578 +0,0 @@
|
|||
|
||||
@font-face {
|
||||
font-family: "Default";
|
||||
src: url("assets/fonts/SF-Pro-Text-Regular.otf");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Compact";
|
||||
src: url("assets/fonts/SF-Compact-Display-Bold.otf");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Rounded";
|
||||
src: url("assets/fonts/SF-Compact-Rounded-Bold.otf");
|
||||
}
|
||||
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
/*background-color: rgba(235, 235, 235, 0.75);*/
|
||||
}
|
||||
|
||||
/* The first word of the class is the component that it modifys. */
|
||||
|
||||
#splash {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#app {
|
||||
position: relative; /* must explicitly be declared */
|
||||
font-family: "Default";
|
||||
-webkit-font-smoothing: antialiased;
|
||||
height: 100vh; /* 100% for website */
|
||||
width: 100vw; /* 100% for website */
|
||||
/* Does not work on Linux */
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
#header-titlebar {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 54px;
|
||||
opacity: 0.75;
|
||||
background-color: #EBEBEB;
|
||||
-webkit-app-region: drag;
|
||||
border: none;
|
||||
outline: none;
|
||||
z-index: 1;
|
||||
border-top-left-radius: 15px;
|
||||
border-top-right-radius: 15px;
|
||||
}
|
||||
|
||||
.contacts {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border-radius: inherit;
|
||||
background-color: #DBDBDB;
|
||||
}
|
||||
|
||||
.contacts > li {
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.contacts img {
|
||||
height: 30px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.contacts .info {
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.contacts .name {
|
||||
|
||||
}
|
||||
|
||||
.contacts .email {
|
||||
font-family: "Compact";
|
||||
font-size: 12px;
|
||||
color: #898989;
|
||||
overflow: w;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
#header-menu {
|
||||
position: absolute;
|
||||
margin-left: 20px;
|
||||
margin-top: 20px;
|
||||
z-index: 2;
|
||||
/* Needed for Linux even with higher Z index */
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
.header-menu-button {
|
||||
min-width: 14px;
|
||||
min-height: 14px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.header-exit-button {
|
||||
background-color: #FF6157;
|
||||
}
|
||||
|
||||
.header-exit-button:active {
|
||||
background: #c14645;
|
||||
}
|
||||
|
||||
.header-min-button {
|
||||
background-color: #FFC12F;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.header-min-button:active {
|
||||
background-color: #c08e38;
|
||||
}
|
||||
|
||||
#login {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#messenger {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* hide native scrollbar */
|
||||
#messenger::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#ws {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 54px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
#ws-logo {
|
||||
transition: all 0.5s;
|
||||
}
|
||||
|
||||
#ws-logo.move {
|
||||
transform: translateX(15px);
|
||||
}
|
||||
|
||||
#ws-dots {
|
||||
opacity: 0;
|
||||
transition: all 0.5s;
|
||||
}
|
||||
|
||||
#ws-dots.move {
|
||||
opacity: 1;
|
||||
transform: translateX(-15px);
|
||||
}
|
||||
|
||||
.ws-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background-color: #48E065;
|
||||
position: relative;
|
||||
transform: translateX(-15px);
|
||||
animation: ws-dot-flashing 1s infinite linear alternate;
|
||||
animation-delay: .25s;
|
||||
}
|
||||
|
||||
.ws-dot::before, .ws-dot::after {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
box-shadow: 0px 0px 10px 0px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
.ws-dot::before {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background-color: #48E065;
|
||||
left: -9px;
|
||||
animation: ws-dot-flashing 1s infinite alternate;
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
.ws-dot::after {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background-color: #48E065;
|
||||
left: 9px;
|
||||
animation: ws-dot-flashing 1s infinite alternate;
|
||||
animation-delay: 0.5s;
|
||||
}
|
||||
|
||||
@keyframes ws-dot-flashing {
|
||||
0% {
|
||||
background-color: #48E065;
|
||||
}
|
||||
50%,
|
||||
100% {
|
||||
background-color: #9B9B9B;
|
||||
}
|
||||
}
|
||||
|
||||
#input {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
z-index: 3;
|
||||
/* To prevent window drag when overlapping with titlebar. */
|
||||
-webkit-app-region: no-drag;
|
||||
border-radius: 50%;
|
||||
/* Set opacity here to not affect child. */
|
||||
background-color: rgba(235, 235, 235, 0.75);
|
||||
box-shadow: 0px 0px 10px 0px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
#input-text {
|
||||
position: absolute;
|
||||
min-width: 150px;
|
||||
height: 16px;
|
||||
border-radius: 18px;
|
||||
padding: 10px;
|
||||
outline: none;
|
||||
border: none;
|
||||
font-size: 14px;
|
||||
pointer-events: none;
|
||||
background-color: white;
|
||||
z-index: -1;
|
||||
box-shadow: 0px 0px 10px 0px rgba(0,0,0,0.15);
|
||||
transform-origin: center;
|
||||
opacity: 0;
|
||||
transform: translateX(70%);
|
||||
transition: all 0.5s;
|
||||
}
|
||||
|
||||
#input-text.show {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#input-text.leftSide {
|
||||
transform: translateX(-70%);
|
||||
}
|
||||
|
||||
#settings {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(235, 235, 235, 0.75);
|
||||
z-index: 4;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
animation-name: settings-appear;
|
||||
animation-duration: 0.5s;
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
@keyframes settings-appear {
|
||||
from {
|
||||
background-color: rgba(235, 235, 235, 0);
|
||||
}
|
||||
to {
|
||||
background-color: rgba(235, 235, 235, 0.75);
|
||||
}
|
||||
}
|
||||
|
||||
.settings-icon {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
border: none;
|
||||
outline: none;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 5px;
|
||||
margin-right: 20px;
|
||||
margin-top: 19px;
|
||||
z-index: 2;
|
||||
background-color: Transparent;
|
||||
/* Needed for Linux */
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
.settings-account {
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.settings-logout-button {
|
||||
font-family: "Compact";
|
||||
background-color: #B7B7B7;
|
||||
padding: 10px 20px;
|
||||
border-radius: 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.settings-version {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 75%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
color: #575757;
|
||||
}
|
||||
|
||||
.message {
|
||||
width: inherit;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-top: 9px;
|
||||
padding-bottom: 9px;
|
||||
animation-name: message-init-anim;
|
||||
animation-duration: 0.25s;
|
||||
}
|
||||
|
||||
@keyframes message-init-anim {
|
||||
from {
|
||||
opacity: 0;
|
||||
} to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.message:first-child {
|
||||
margin-top: 55px;
|
||||
}
|
||||
|
||||
.message:last-child {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.message-session {
|
||||
width: 100vw;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-family: "Compact";
|
||||
font-size: 12px;
|
||||
color: #9B9B9B;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.client-message {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.ai-message {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.admin-message {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.bubble {
|
||||
position: relative;
|
||||
max-width: 66%;
|
||||
font-size: 14px;
|
||||
border-radius: 18px;
|
||||
margin-bottom: 4px;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.bubble-notify {
|
||||
position: absolute;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background-color: #58D9FF;
|
||||
top: -5px;
|
||||
left: -5px;
|
||||
border: 2px solid #EBEBEB;
|
||||
transform: scale(0);
|
||||
animation-name: bubble-notify-anim;
|
||||
animation-duration: 5s;
|
||||
}
|
||||
|
||||
@keyframes bubble-notify-anim {
|
||||
0%, 90% {
|
||||
transform: scale(1);
|
||||
}
|
||||
100% {
|
||||
transform: scale(0);
|
||||
}
|
||||
}
|
||||
|
||||
.ai-bubble {
|
||||
background-color: #FFFFFF;
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
.client-bubble {
|
||||
color: white;
|
||||
background-color: #58C4FD;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.ai-first-child {
|
||||
border-bottom-left-radius: 9px;
|
||||
}
|
||||
|
||||
.ai-middle-child {
|
||||
border-top-left-radius: 9px;
|
||||
border-bottom-left-radius: 9px;
|
||||
}
|
||||
|
||||
.ai-last-child {
|
||||
border-top-left-radius: 9px;
|
||||
}
|
||||
|
||||
.client-first-child {
|
||||
border-bottom-right-radius: 9px;
|
||||
}
|
||||
|
||||
.client-middle-child {
|
||||
border-top-right-radius: 9px;
|
||||
border-bottom-right-radius: 9px;
|
||||
}
|
||||
|
||||
.client-last-child {
|
||||
border-top-right-radius: 9px;
|
||||
}
|
||||
|
||||
.bubble > p {
|
||||
margin: 0px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.bubble > div {
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
.bubble > img {
|
||||
border-radius: inherit;
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.bubble a {
|
||||
display: inline-block;
|
||||
font-family: "Rounded";
|
||||
text-decoration: none;
|
||||
border-radius: inherit;
|
||||
background-color: #D9D9D9;
|
||||
padding: 10px;
|
||||
color: #727272;
|
||||
}
|
||||
|
||||
.context {
|
||||
position: relative;
|
||||
min-height: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-family: "Compact";
|
||||
font-size: 12px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.ai-context {
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
.client-context {
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.context-avatar {
|
||||
margin-right: 5px;
|
||||
border-radius: 50%;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
/* ---------- shared between components ---------- */
|
||||
|
||||
.button {
|
||||
border: none;
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/*.button:hover {
|
||||
cursor: pointer;
|
||||
}*/
|
||||
|
||||
/* shake a div to signal error*/
|
||||
.shake {
|
||||
animation: shake 0.82s cubic-bezier(.36,.07,.19,.97) both;
|
||||
transform: translate3d(0, 0, 0);
|
||||
backface-visibility: hidden;
|
||||
perspective: 1000px;
|
||||
}
|
||||
|
||||
@keyframes shake {
|
||||
10%, 90% {
|
||||
transform: translate3d(-1px, 0, 0);
|
||||
}
|
||||
|
||||
20%, 80% {
|
||||
transform: translate3d(2px, 0, 0);
|
||||
}
|
||||
|
||||
30%, 50%, 70% {
|
||||
transform: translate3d(-4px, 0, 0);
|
||||
}
|
||||
|
||||
40%, 60% {
|
||||
transform: translate3d(4px, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
.audio {
|
||||
animation-name: audio-anim;
|
||||
animation-duration: 2s;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
@keyframes audio-anim {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
.smart-form {
|
||||
width: 225px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.text-field {
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
background: none;
|
||||
border: none;
|
||||
margin: 4px;
|
||||
font-size: 14px;
|
||||
border-radius: 10px;
|
||||
background-color: #d1d1d1;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.smart-form-logs {
|
||||
font-family: "Compact";
|
||||
font-size: 12px;
|
||||
text-align: left;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
const { contextBridge, ipcRenderer } = require("electron");
|
||||
|
||||
/** The main <-> render interface */
|
||||
|
||||
const validFromMainChannels = [
|
||||
"playback",
|
||||
"account",
|
||||
"message",
|
||||
"record",
|
||||
"ws"
|
||||
];
|
||||
|
||||
const validToMain = [
|
||||
"messenger",
|
||||
"message",
|
||||
"logout",
|
||||
"encode",
|
||||
"auth",
|
||||
"nav"
|
||||
]
|
||||
|
||||
ipcRenderer.setMaxListeners(250);
|
||||
|
||||
// Expose protected methods that allow the renderer process to use
|
||||
// the ipcRenderer without exposing the entire object
|
||||
// TODO: Implement argument filtering for added security
|
||||
contextBridge.exposeInMainWorld(
|
||||
"mainApi", {
|
||||
invoke: async (channel, ...args) => {
|
||||
if (validToMain.includes(channel)) {
|
||||
try {
|
||||
const res = await ipcRenderer.invoke(channel, ...args);
|
||||
return res;
|
||||
} catch (e) {
|
||||
console.log("IPCRenderer.invoke error");
|
||||
}
|
||||
}
|
||||
},
|
||||
send: (channel, ...args) => {
|
||||
if (validToMain.includes(channel)) {
|
||||
ipcRenderer.send(channel, ...args);
|
||||
}
|
||||
},
|
||||
on: (channel, func) => {
|
||||
if (validFromMainChannels.includes(channel)) {
|
||||
ipcRenderer.on(channel, (event, ...args) => func(...args));
|
||||
}
|
||||
},
|
||||
removeAllListeners: (channel) => {
|
||||
ipcRenderer.removeAllListeners(channel);
|
||||
}
|
||||
}
|
||||
);
|
||||