Compare commits
104 changed files with 4134 additions and 3860 deletions
3
.browserslistrc
Normal file
3
.browserslistrc
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
> 1%
|
||||||
|
last 2 versions
|
||||||
|
not dead
|
||||||
10
.gitignore
vendored
10
.gitignore
vendored
|
|
@ -4,12 +4,11 @@ node_modules
|
||||||
/crate/target/*
|
/crate/target/*
|
||||||
/.log
|
/.log
|
||||||
|
|
||||||
crash.log
|
rawAudio.wav
|
||||||
|
|
||||||
# local env files
|
# local env files
|
||||||
.env.local
|
.env.local
|
||||||
.env.*.local
|
.env.*.local
|
||||||
.env
|
|
||||||
|
|
||||||
# Log files
|
# Log files
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
|
|
@ -28,10 +27,3 @@ pnpm-debug.log*
|
||||||
|
|
||||||
#Electron-builder output
|
#Electron-builder output
|
||||||
/dist_electron
|
/dist_electron
|
||||||
|
|
||||||
#Electron-builder config
|
|
||||||
electron-builder.yml
|
|
||||||
|
|
||||||
#Window and session states
|
|
||||||
session.json
|
|
||||||
window.json
|
|
||||||
|
|
|
||||||
|
|
@ -1,63 +0,0 @@
|
||||||
stages:
|
|
||||||
- Build
|
|
||||||
- Upload
|
|
||||||
- Release
|
|
||||||
|
|
||||||
build:
|
|
||||||
stage: Build
|
|
||||||
tags:
|
|
||||||
# Use a macos runner to build.
|
|
||||||
- darwin
|
|
||||||
before_script:
|
|
||||||
- yarn
|
|
||||||
script:
|
|
||||||
- export VERSION=$(node -e "console.log(require('./package.json').version)")
|
|
||||||
- echo "VERSION=$VERSION" >> variables.env
|
|
||||||
- export APPNAME=$(node -e "console.log(require('./package.json').productName)")
|
|
||||||
- echo "APPNAME=$APPNAME" >> variables.env
|
|
||||||
- yarn build
|
|
||||||
artifacts:
|
|
||||||
reports:
|
|
||||||
dotenv: variables.env
|
|
||||||
name: $CI_COMMIT_REF_SLUG
|
|
||||||
paths:
|
|
||||||
- dist_electron/*.dmg
|
|
||||||
- dist_electron/*.zip
|
|
||||||
- dist_electron/*.yml
|
|
||||||
when: on_success
|
|
||||||
only:
|
|
||||||
- main
|
|
||||||
|
|
||||||
variables:
|
|
||||||
PACKAGE: '${APPNAME}-${VERSION}.dmg'
|
|
||||||
PACKAGE_REGISTRY_URL: '${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/${APPNAME}/${VERSION}'
|
|
||||||
|
|
||||||
# Upload package to gitlab registry.
|
|
||||||
upload:
|
|
||||||
stage: Upload
|
|
||||||
needs:
|
|
||||||
- job: build
|
|
||||||
artifacts: true
|
|
||||||
rules:
|
|
||||||
- if: $CI_COMMIT_TAG
|
|
||||||
when: never # Do not run this job when a tag is created manually
|
|
||||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH # Run this job when when commits are pushed to the default branch
|
|
||||||
script:
|
|
||||||
# Take the package we built and place it in the package registry.
|
|
||||||
- 'curl --header "JOB-TOKEN: $CI_JOB_TOKEN" --upload-file "dist_electron/${PACKAGE}" "${PACKAGE_REGISTRY_URL}/${PACKAGE}"'
|
|
||||||
|
|
||||||
auto-release-master:
|
|
||||||
image: registry.gitlab.com/gitlab-org/release-cli
|
|
||||||
needs:
|
|
||||||
- job: build
|
|
||||||
artifacts: true
|
|
||||||
- job: upload
|
|
||||||
artifacts: true
|
|
||||||
stage: Release
|
|
||||||
rules:
|
|
||||||
- if: $CI_COMMIT_TAG
|
|
||||||
when: never # Do not run this job when a tag is created manually
|
|
||||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH # Run this job when when commits are pushed to the default branch
|
|
||||||
script:
|
|
||||||
- echo "Release $VERSION"
|
|
||||||
- release-cli create --name "Release $VERSION" --tag-name v$VERSION --description "Release $CI_COMMIT_TITLE" --ref $CI_COMMIT_SHA --assets-link "{\"name\":\"${APPNAME}\",\"url\":\"${PACKAGE_REGISTRY_URL}/${PACKAGE}\"}"
|
|
||||||
|
|
@ -7,7 +7,7 @@ yarn install
|
||||||
|
|
||||||
### Compiles and hot-reloads for development
|
### Compiles and hot-reloads for development
|
||||||
```
|
```
|
||||||
yarn electron:serve
|
yarn dev
|
||||||
```
|
```
|
||||||
|
|
||||||
### Compiles and minifies for production
|
### Compiles and minifies for production
|
||||||
|
|
@ -15,7 +15,7 @@ yarn electron:serve
|
||||||
yarn electron:build
|
yarn electron:build
|
||||||
```
|
```
|
||||||
|
|
||||||
<!-- ### Run your unit tests
|
### Run your unit tests
|
||||||
```
|
```
|
||||||
yarn test:unit
|
yarn test:unit
|
||||||
```
|
```
|
||||||
|
|
@ -23,7 +23,7 @@ yarn test:unit
|
||||||
### Lints and fixes files
|
### Lints and fixes files
|
||||||
```
|
```
|
||||||
yarn lint
|
yarn lint
|
||||||
``` -->
|
```
|
||||||
|
|
||||||
### Customize configuration
|
### Customize configuration
|
||||||
See [Configuration Reference](https://cli.vuejs.org/config/).
|
See [Configuration Reference](https://cli.vuejs.org/config/).
|
||||||
|
|
|
||||||
79
build/config.gypi
Normal file
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
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]
|
||||||
45
login.svg
Normal file
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 |
55
package.json
55
package.json
|
|
@ -1,18 +1,24 @@
|
||||||
{
|
{
|
||||||
"name": "Crimata",
|
"name": "Crimata",
|
||||||
"version": "0.9.9",
|
"productName": "crimata-messenger",
|
||||||
"private": true,
|
|
||||||
"description": "Cross-platform messenger application built with electron, vue3, and TS.",
|
"description": "Cross-platform messenger application built with electron, vue3, and TS.",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Enrique Hernandez"
|
"name": "Enrique Hernandez"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "vue-cli-service electron:build",
|
"dev": "yarn electron:serve",
|
||||||
"dev": "vue-cli-service 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",
|
"postinstall": "electron-builder install-app-deps",
|
||||||
"postuninstall": "electron-builder install-app-deps"
|
"postuninstal": "electron-builder install-app-deps"
|
||||||
},
|
},
|
||||||
"main": "init.js",
|
"main": "background.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@google-cloud/speech": "^4.2.0",
|
"@google-cloud/speech": "^4.2.0",
|
||||||
"@types/animejs": "^3.1.2",
|
"@types/animejs": "^3.1.2",
|
||||||
|
|
@ -22,24 +28,16 @@
|
||||||
"@types/uuid": "^8.3.0",
|
"@types/uuid": "^8.3.0",
|
||||||
"@types/ws": "^7.2.7",
|
"@types/ws": "^7.2.7",
|
||||||
"animejs": "^3.2.0",
|
"animejs": "^3.2.0",
|
||||||
"axios": "^0.21.1",
|
|
||||||
"core-js": "^3.6.5",
|
"core-js": "^3.6.5",
|
||||||
"electron-is-dev": "^2.0.0",
|
|
||||||
"electron-store": "^8.0.0",
|
|
||||||
"electron-updater": "^4.3.8",
|
|
||||||
"mitt": "^2.1.0",
|
"mitt": "^2.1.0",
|
||||||
"naudiodon": "^2.3.2",
|
"naudiodon": "^2.3.2",
|
||||||
"node-record-lpcm16": "^1.0.1",
|
"node-record-lpcm16": "^1.0.1",
|
||||||
"update-electron-app": "^2.0.1",
|
|
||||||
"uuid": "^8.3.2",
|
|
||||||
"vue": "^3.0.0-0",
|
"vue": "^3.0.0-0",
|
||||||
"vue-router": "^4.0.0-0",
|
"vue-router": "^4.0.0-0",
|
||||||
"vuex": "^4.0.0-0",
|
"vuex": "^4.0.0-0",
|
||||||
"ws": "^7.3.1"
|
"ws": "^7.3.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/axios": "^0.14.0",
|
|
||||||
"@types/electron-devtools-installer": "^2.2.0",
|
|
||||||
"@types/jest": "^24.0.19",
|
"@types/jest": "^24.0.19",
|
||||||
"@typescript-eslint/eslint-plugin": "^2.33.0",
|
"@typescript-eslint/eslint-plugin": "^2.33.0",
|
||||||
"@typescript-eslint/parser": "^2.33.0",
|
"@typescript-eslint/parser": "^2.33.0",
|
||||||
|
|
@ -56,36 +54,17 @@
|
||||||
"@wasm-tool/wasm-pack-plugin": "^1.3.1",
|
"@wasm-tool/wasm-pack-plugin": "^1.3.1",
|
||||||
"electron": "^9.0.0",
|
"electron": "^9.0.0",
|
||||||
"electron-devtools-installer": "^3.1.0",
|
"electron-devtools-installer": "^3.1.0",
|
||||||
"electron-log": "^4.3.4",
|
|
||||||
"eslint": "^6.7.2",
|
"eslint": "^6.7.2",
|
||||||
"eslint-plugin-vue": "^7.0.0-0",
|
"eslint-plugin-vue": "^7.0.0-0",
|
||||||
"lint-staged": "^9.5.0",
|
"lint-staged": "^9.5.0",
|
||||||
"node-sass": "^4.12.0",
|
"node-sass": "^4.12.0",
|
||||||
"optimize-wasm-webpack-plugin": "^1.0.12",
|
"optimize-wasm-webpack-plugin": "^1.0.12",
|
||||||
"sass-loader": "^8.0.2",
|
"sass-loader": "^8.0.2",
|
||||||
"spectron": "11.0.0",
|
"spectron": "^11.0.0",
|
||||||
"typescript": "~3.9.3",
|
"typescript": "~3.9.3",
|
||||||
"vue-cli-plugin-electron-builder": "~2.0.0-rc.6",
|
"vue-cli-plugin-electron-builder": "~2.0.0-rc.4",
|
||||||
"vue-jest": "^5.0.0-0"
|
"vue-jest": "^5.0.0-0"
|
||||||
},
|
},
|
||||||
"vue": {
|
|
||||||
"lintOnSave": false,
|
|
||||||
"pluginOptions": {
|
|
||||||
"electronBuilder": {
|
|
||||||
"mainProcessFile": "./src/init.ts",
|
|
||||||
"rendererProcessFile": "./src/render/main.ts",
|
|
||||||
"preload": "./src/render/preload.ts",
|
|
||||||
"builderOptions": {
|
|
||||||
"appId": "com.crimata.ElectronUpdaterApp",
|
|
||||||
"artifactName": "${productName}-${version}.${ext}",
|
|
||||||
"publish": {
|
|
||||||
"provider": "generic",
|
|
||||||
"url": "https://gitlab.com/api/v4/projects/25637892/jobs/artifacts/main/raw/dist_electron?job=build"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"gitHooks": {
|
"gitHooks": {
|
||||||
"pre-commit": "lint-staged"
|
"pre-commit": "lint-staged"
|
||||||
},
|
},
|
||||||
|
|
@ -94,11 +73,5 @@
|
||||||
"vue-cli-service lint",
|
"vue-cli-service lint",
|
||||||
"git add"
|
"git add"
|
||||||
]
|
]
|
||||||
},
|
|
||||||
"productName": "crimata-messenger",
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://gitlab.com/crimata/electron-app.git",
|
|
||||||
"release": "latest"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,10 @@
|
||||||
<noscript>
|
<noscript>
|
||||||
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||||
</noscript>
|
</noscript>
|
||||||
|
<!-- <h1>Hello World!</h1>
|
||||||
|
We are using Node.js <span id="node-version"></span>, Chromium
|
||||||
|
<span id="chrome-version"></span>, and Electron
|
||||||
|
<span id="electron-version"></span>. -->
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<!-- built files will be auto injected -->
|
<!-- built files will be auto injected -->
|
||||||
</body>
|
</body>
|
||||||
|
|
|
||||||
94
src/App.vue
Normal file
94
src/App.vue
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
<template>
|
||||||
|
<div id="app">
|
||||||
|
|
||||||
|
<button class="titlebar">
|
||||||
|
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Menu -->
|
||||||
|
<span class="menu">
|
||||||
|
<button class="menuButton exitButton"></button>
|
||||||
|
<button class="menuButton minimizeButton"></button>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- Windows -->
|
||||||
|
<router-view/>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<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,95 +0,0 @@
|
||||||
|
|
||||||
import { postAuth, postLogin, postLogout } from "@/api/account";
|
|
||||||
import { endSession, launchSession } from "@/session";
|
|
||||||
import { getToken, setToken, setProfile, getProfile, clearStore } from "./store";
|
|
||||||
import { parseAuthRes } from "./auth";
|
|
||||||
import { ipcEmit } from "@/composables/useEmitter";
|
|
||||||
|
|
||||||
export const accountAuth = async (): Promise<Error | AuthState> => {
|
|
||||||
|
|
||||||
/* attempt to get a login token from the store */
|
|
||||||
const token = getToken();
|
|
||||||
|
|
||||||
/* try to login with it, returns platform secret and new token on success */
|
|
||||||
if (token) {
|
|
||||||
try {
|
|
||||||
|
|
||||||
const res = await postAuth(token);
|
|
||||||
|
|
||||||
const parsed = parseAuthRes(res);
|
|
||||||
|
|
||||||
setToken(parsed.token)
|
|
||||||
setProfile(parsed.profile);
|
|
||||||
|
|
||||||
return {
|
|
||||||
profile: parsed.profile,
|
|
||||||
token: parsed.token
|
|
||||||
};
|
|
||||||
|
|
||||||
} catch(e) {
|
|
||||||
console.log('[ACCOUNT]', e);
|
|
||||||
clearStore();
|
|
||||||
throw(new Error('Failed to authenticate.'));
|
|
||||||
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw(new Error('Unable to authenticate.'));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const accountLogin: IpcHandlerCallback<AccountCredentials, Profile> = async (payload) => {
|
|
||||||
const account = payload as AccountCredentials;
|
|
||||||
try {
|
|
||||||
|
|
||||||
// attempt login with email password
|
|
||||||
const res = await postLogin(account.email, account.password);
|
|
||||||
const parsed = parseAuthRes(res);
|
|
||||||
|
|
||||||
// save jwt token and profile
|
|
||||||
setToken(parsed.token);
|
|
||||||
setProfile(parsed.profile);
|
|
||||||
|
|
||||||
// launch session
|
|
||||||
launchSession(parsed.token);
|
|
||||||
|
|
||||||
// return profile to renderer
|
|
||||||
return parsed.profile;
|
|
||||||
|
|
||||||
} catch(e) {
|
|
||||||
clearStore();
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export const accountLogout = async (): Promise<Error | void> => {
|
|
||||||
|
|
||||||
try {
|
|
||||||
// post logout to backend
|
|
||||||
await postLogout();
|
|
||||||
|
|
||||||
// remove key and crimataId
|
|
||||||
clearStore();
|
|
||||||
|
|
||||||
// kill crimata platform session
|
|
||||||
endSession();
|
|
||||||
|
|
||||||
return;
|
|
||||||
|
|
||||||
} catch(e) {
|
|
||||||
console.log('[ACCOUNT]', e);
|
|
||||||
return (new Error('Failed to logout. Please try again.'));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export const updateAppState = (): void => {
|
|
||||||
|
|
||||||
const profile = getProfile();
|
|
||||||
|
|
||||||
ipcEmit("set-profile", profile);
|
|
||||||
|
|
||||||
// ipcEmit('messages') etc
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
|
|
||||||
import useHttp from "@/composables/useHttp";
|
|
||||||
import axios from "axios";
|
|
||||||
import {config} from "@/config";
|
|
||||||
|
|
||||||
const { post } = useHttp();
|
|
||||||
|
|
||||||
export const postAuth = async (token: string) => (
|
|
||||||
await axios({
|
|
||||||
url: config.BUSINESS_URL + config.BUSINESS_PREFIX + '/account/authenticate',
|
|
||||||
headers: {
|
|
||||||
Cookie: `crimataCookie=${token}`
|
|
||||||
},
|
|
||||||
method: 'POST',
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
export const postLogin = async (email: string, password: string) => (
|
|
||||||
await post('/account/login', { email, password })
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
export const postLogout =
|
|
||||||
async (): Promise<null | Error> => (await post('/account/logout'));
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
14
src/assets/crimata.svg
Normal file
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 |
187
src/audio.ts
187
src/audio.ts
|
|
@ -1,187 +0,0 @@
|
||||||
/* eslint @typescript-eslint/no-var-requires: "off" */
|
|
||||||
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
// where the audio goes
|
|
||||||
let buffer: ArrayBuffer[] = [];
|
|
||||||
|
|
||||||
// place audio data in buffer
|
|
||||||
export const collect: IpcListenerCallback<ArrayBuffer> = (chunk) => {
|
|
||||||
if (chunk) buffer.push(chunk);
|
|
||||||
}
|
|
||||||
|
|
||||||
// return audio and clear buffer
|
|
||||||
export const flush = async () => {
|
|
||||||
const bufferCopy = buffer;
|
|
||||||
buffer = [];
|
|
||||||
return bufferCopy;
|
|
||||||
}
|
|
||||||
|
|
||||||
// import { backgroundMitt } from '@/modules/emitter';
|
|
||||||
// const portAudio = require('naudiodon');
|
|
||||||
|
|
||||||
// // Audio in and out stream objects.
|
|
||||||
// let ai: typeof portAudio.AudioIO | boolean = false;
|
|
||||||
// let ao: typeof portAudio.AudioIO | boolean = false;
|
|
||||||
|
|
||||||
// // Whether activly recording.
|
|
||||||
// let record = false;
|
|
||||||
|
|
||||||
// const audioContainer = {
|
|
||||||
// input: '',
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const audioOptions = {
|
|
||||||
// channelCount: 1,
|
|
||||||
// sampleFormat: 16,
|
|
||||||
// sampleRate: 16000,
|
|
||||||
// deviceId: -1,
|
|
||||||
// closeOnError: false,
|
|
||||||
// }
|
|
||||||
|
|
||||||
// export const toggleRecord = (): void => { record = !record };
|
|
||||||
|
|
||||||
|
|
||||||
// export const fetchAudioInput = (): Promise<Error | string> => (
|
|
||||||
|
|
||||||
// new Promise((resolve, reject) => {
|
|
||||||
|
|
||||||
// try {
|
|
||||||
// resolve(audioContainer.input);
|
|
||||||
// toggleRecord();
|
|
||||||
// } catch (e) {
|
|
||||||
// reject(new Error('Failed to fetch the audio.'))
|
|
||||||
// }
|
|
||||||
|
|
||||||
// })
|
|
||||||
// )
|
|
||||||
|
|
||||||
|
|
||||||
// // Main audio function run by run.ts module.
|
|
||||||
// export function initAudioIO(): void {
|
|
||||||
// console.log("AUDIO:Starting io streams.")
|
|
||||||
|
|
||||||
// if (!ai) {
|
|
||||||
|
|
||||||
// // Initialize and start input stream.
|
|
||||||
// ai = new portAudio.AudioIO({ inOptions: audioOptions });
|
|
||||||
// ai.setEncoding("hex");
|
|
||||||
// ai.start();
|
|
||||||
|
|
||||||
// // On each data chunk...
|
|
||||||
// ai.on('data', (chunk: string) => {
|
|
||||||
|
|
||||||
// // If recording, we capture the data.
|
|
||||||
// if (record) {
|
|
||||||
// console.log('AUDIO:Recording...')
|
|
||||||
// audioContainer.input += chunk;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Else, we don't capture and also clear audioContainer.
|
|
||||||
// else {
|
|
||||||
// if (audioContainer.input.length) {
|
|
||||||
// audioContainer.input = "";
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if (!ao) {
|
|
||||||
|
|
||||||
// // Initialize and start input stream.
|
|
||||||
// ao = new portAudio.AudioIO({ outOptions: audioOptions });
|
|
||||||
// ao.start();
|
|
||||||
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
|
||||||
// // ---Audio playback--------------------------------------------
|
|
||||||
|
|
||||||
// // Split Buffer into an array of len-sized Buffers.
|
|
||||||
// function bufSplit(buf: Buffer, len: number): Array<Buffer> {
|
|
||||||
// const chunks = [];
|
|
||||||
// let i = 0;
|
|
||||||
// let L = len;
|
|
||||||
|
|
||||||
// while(i < buf.byteLength) {
|
|
||||||
// chunks.push(buf.slice(i, L));
|
|
||||||
// i = L;
|
|
||||||
// L += len;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return chunks;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Audio playback.
|
|
||||||
// export function play(input: string): void {
|
|
||||||
|
|
||||||
// // Format the audio.
|
|
||||||
// const audio = bufSplit(
|
|
||||||
// Buffer.from(input as string, 'hex'),
|
|
||||||
// 8192
|
|
||||||
// );
|
|
||||||
|
|
||||||
// // Called on end of write.
|
|
||||||
// const callback = () => {
|
|
||||||
|
|
||||||
// // We stop audio playback anim.
|
|
||||||
// backgroundMitt.emit('ipc-renderer', {
|
|
||||||
// endpoint: 'stop-playback-anim'
|
|
||||||
// });
|
|
||||||
|
|
||||||
// }
|
|
||||||
|
|
||||||
// write();
|
|
||||||
|
|
||||||
// // Iterate through audio array and write buffers to portAudio writable.
|
|
||||||
// function write() {
|
|
||||||
// let chunk: Buffer;
|
|
||||||
// let ok = true;
|
|
||||||
// let i = 0;
|
|
||||||
|
|
||||||
// do {
|
|
||||||
// chunk = audio[i];
|
|
||||||
// if (i === audio.length - 1) {
|
|
||||||
// // write last chunk.
|
|
||||||
// ao.write(chunk, null, callback);
|
|
||||||
// } else {
|
|
||||||
// // check for backpreassure.
|
|
||||||
// ok = ao.write(chunk, null);
|
|
||||||
// }
|
|
||||||
// i++;
|
|
||||||
// } while (i < audio.length && ok);
|
|
||||||
|
|
||||||
// if (i < audio.length) {
|
|
||||||
// // Had to stop early!
|
|
||||||
// // Write some more once it drains.
|
|
||||||
// ao.once('drain', write);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // -------------------------------------------------------------
|
|
||||||
|
|
||||||
// // Get's called on window close.
|
|
||||||
// export async function stopStream() {
|
|
||||||
// console.log("AUDIO:Stopping audio stream.")
|
|
||||||
// if (ai) {
|
|
||||||
// try {
|
|
||||||
// await ai.quit()
|
|
||||||
// } catch(e){
|
|
||||||
// console.log('AUDIO: Failed to shutdown audio input.');
|
|
||||||
// throw e;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// if (ao) {
|
|
||||||
// try {
|
|
||||||
// await ao.quit()
|
|
||||||
// } catch(e){
|
|
||||||
// console.log('AUDIO: Failed to shutdown audio output.');
|
|
||||||
// throw e;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
|
||||||
13
src/auth.ts
13
src/auth.ts
|
|
@ -1,13 +0,0 @@
|
||||||
|
|
||||||
export const parseAuthRes = (authRes: any) => {
|
|
||||||
const token = authRes.headers['set-cookie'][0].split(";")[0].split("=")[1] as string;
|
|
||||||
const profile = authRes.data as Profile;
|
|
||||||
return {
|
|
||||||
token,
|
|
||||||
profile
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
25
src/background.ts
Normal file
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);
|
||||||
|
|
||||||
|
})();
|
||||||
50
src/background/audio.ts
Normal file
50
src/background/audio.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
"use strict";
|
||||||
|
/* eslint @typescript-eslint/no-var-requires: "off" */
|
||||||
|
import { sendAudio } from './session'
|
||||||
|
const portAudio = require('naudiodon');
|
||||||
|
|
||||||
|
let ia: typeof portAudio.AudioIO;
|
||||||
|
let audioInput: string[] = [];
|
||||||
|
|
||||||
|
function processAudio(audioInput: Array<string>): Buffer {
|
||||||
|
let audio = '';
|
||||||
|
audioInput.forEach(chunk => audio += chunk);
|
||||||
|
return Buffer.from(audio, "base64");
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateRecorder = (_event, record: boolean): void => {
|
||||||
|
if (record) {
|
||||||
|
ia.resume();
|
||||||
|
} else {
|
||||||
|
ia.pause();
|
||||||
|
const audio = processAudio(audioInput);
|
||||||
|
sendAudio(audio);
|
||||||
|
audioInput = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const initRecorder = () => {
|
||||||
|
|
||||||
|
if (!ia) {
|
||||||
|
ia = new portAudio.AudioIO({
|
||||||
|
inOptions: {
|
||||||
|
channelCount: 1,
|
||||||
|
sampleFormat: 16,
|
||||||
|
sampleRate: 16000,
|
||||||
|
deviceId: -1,
|
||||||
|
closeOnError: false,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ia.setEncoding('base64');
|
||||||
|
ia.start();
|
||||||
|
ia.pause();
|
||||||
|
ia.on('error', (e: Error) => {
|
||||||
|
console.log('Error recording audio', + e);
|
||||||
|
});
|
||||||
|
ia.on('data', (chunk: string) => {
|
||||||
|
audioInput.push(chunk);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
6
src/background/emitter.ts
Normal file
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
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.on('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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: Debug activate functionaity: currently not working.
|
||||||
|
// 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
28
src/background/run.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
import { createWindow } from './window';
|
||||||
|
import { initSession } from './session';
|
||||||
|
import { initRecorder } 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.
|
||||||
|
// initRecorder();
|
||||||
|
|
||||||
|
}
|
||||||
143
src/background/session.ts
Normal file
143
src/background/session.ts
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
import WebSocket from 'ws';
|
||||||
|
import { backgroundMitt } from './emitter';
|
||||||
|
import { ipcMain } from "electron";
|
||||||
|
|
||||||
|
interface RenderMessage {
|
||||||
|
content: string;
|
||||||
|
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);
|
||||||
|
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);
|
||||||
|
};
|
||||||
|
|
||||||
89
src/background/window.ts
Normal file
89
src/background/window.ts
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
import { BrowserWindow, ipcMain } from "electron";
|
||||||
|
import { createProtocol } from "vue-cli-plugin-electron-builder/lib";
|
||||||
|
import { backgroundMitt } from './emitter';
|
||||||
|
import { updateRecorder } from './audio';
|
||||||
|
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;
|
||||||
|
|
||||||
|
const windowMount = (): void => {
|
||||||
|
backgroundMitt.emit('window-active', true);
|
||||||
|
ipcMain.on('update-recorder', updateRecorder);
|
||||||
|
}
|
||||||
|
|
||||||
|
const windowDismount = (): void => {
|
||||||
|
win = null;
|
||||||
|
backgroundMitt.emit('window-active', false);
|
||||||
|
ipcMain.removeAllListeners('update-recorder');
|
||||||
|
}
|
||||||
|
|
||||||
|
backgroundMitt.on('ipc-renderer', (payload: IpcRendererPayload) => {
|
||||||
|
if (win)
|
||||||
|
win.webContents.send(payload.endpoint, {
|
||||||
|
message: payload.message
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
export const createWindow = async (options: WindowSettings): Promise<void> => {
|
||||||
|
return new Promise((resolve, _reject) => {
|
||||||
|
|
||||||
|
if (win) resolve();
|
||||||
|
|
||||||
|
win = new BrowserWindow({
|
||||||
|
width: options.width,
|
||||||
|
height: options.height,
|
||||||
|
resizable: options.resizable,
|
||||||
|
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.webContents.on('did-finish-load', () => {
|
||||||
|
windowMount();
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
60
src/components/messageView/anime.ts
Normal file
60
src/components/messageView/anime.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
import { inject } from "vue";
|
||||||
|
import AnimeFunc from "@/types/animejs/index";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* anime js animations used by message view transitions
|
||||||
|
*/
|
||||||
|
export default function useMessageViewAnims() {
|
||||||
|
// imports animejs safely
|
||||||
|
let anime: AnimeFunc;
|
||||||
|
const animeInject: AnimeFunc | undefined = inject("animejs");
|
||||||
|
if (animeInject) anime = animeInject;
|
||||||
|
|
||||||
|
// update message View bottom bound
|
||||||
|
const shiftView = (el: SVGGElement, transform: string) => {
|
||||||
|
anime({
|
||||||
|
targets: el,
|
||||||
|
transform: transform,
|
||||||
|
easing: "easeInOutQuad",
|
||||||
|
duration: 500,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// stacks message from top to bottom
|
||||||
|
const stackAnimate = (el: Element, transform: string) => {
|
||||||
|
return new Promise((resolve, _reject) => {
|
||||||
|
anime({
|
||||||
|
targets: el,
|
||||||
|
transform: transform,
|
||||||
|
easing: "easeInOutQuad",
|
||||||
|
duration: 1000,
|
||||||
|
complete: function() {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// adds new message at the bottom of the list and shifts existing message list
|
||||||
|
const shiftAnimate = (el: Element, transform: string, view: SVGGElement, viewTransform: string) => {
|
||||||
|
return new Promise((resolve, _reject) => {
|
||||||
|
anime({
|
||||||
|
targets: el,
|
||||||
|
transform: transform,
|
||||||
|
easing: "easeInOutQuad",
|
||||||
|
duration: 800,
|
||||||
|
begin: function() {
|
||||||
|
shiftView(view, viewTransform);
|
||||||
|
},
|
||||||
|
complete: function() {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
shiftAnimate,
|
||||||
|
stackAnimate,
|
||||||
|
};
|
||||||
|
}
|
||||||
90
src/components/messageView/index.vue
Normal file
90
src/components/messageView/index.vue
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
<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 { Queue } from "@/modules/queue";
|
||||||
|
import useTransitions from "./viewDetails/transitions";
|
||||||
|
import useScrollView from "@/modules/scrollView";
|
||||||
|
import { defineComponent, reactive, onMounted, onUnmounted, watch } from "vue";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: "MessageView",
|
||||||
|
components: { TextBubble },
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
const renderArr: Message[] = reactive([]);
|
||||||
|
const renderQ = new Queue();
|
||||||
|
|
||||||
|
// 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(() => {
|
||||||
|
window.ipcRenderer.on("render-message", (event, payload) => {
|
||||||
|
const m = payload.message;
|
||||||
|
if (m.content) render(m);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.ipcRenderer.removeAllListeners("render-message");
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
renderArr,
|
||||||
|
beforeEnter,
|
||||||
|
enter,
|
||||||
|
afterEnter,
|
||||||
|
scrollView,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
#messageView {
|
||||||
|
width: 1000px;
|
||||||
|
height: 100vh;
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
33
src/components/messageView/message/calcPosition.ts
Normal file
33
src/components/messageView/message/calcPosition.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
import {computed} from "vue";
|
||||||
|
|
||||||
|
// Size of "side-gutters" of message window.
|
||||||
|
const margin = 20;
|
||||||
|
const messagePadding = 15;
|
||||||
|
const firstChildMargin = 45;
|
||||||
|
|
||||||
|
|
||||||
|
function calcXPosition(messageWidth: number, windowWidth: number, modifier: string) {
|
||||||
|
let offset = margin;
|
||||||
|
|
||||||
|
if (modifier == "sf") {
|
||||||
|
offset = windowWidth - messageWidth - margin;
|
||||||
|
}
|
||||||
|
|
||||||
|
return offset
|
||||||
|
}
|
||||||
|
|
||||||
|
function calcMessagePosition(modifier: string, messageWidth: number,
|
||||||
|
windowWidth: number, anchorPosition: any) {
|
||||||
|
|
||||||
|
const X = calcXPosition(messageWidth, windowWidth, modifier);
|
||||||
|
|
||||||
|
// Depends on if first child.
|
||||||
|
let Y = firstChildMargin;
|
||||||
|
if (anchorPosition != "none") {
|
||||||
|
Y = anchorPosition + messagePadding;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `translate(${X} ${Y})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default calcMessagePosition;
|
||||||
101
src/components/messageView/message/message.vue
Normal file
101
src/components/messageView/message/message.vue
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
<template>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="100"
|
||||||
|
height="46"
|
||||||
|
:transform="position"
|
||||||
|
>
|
||||||
|
|
||||||
|
<rect
|
||||||
|
:id="message.id"
|
||||||
|
:class="message.modifier"
|
||||||
|
|
||||||
|
width="100" height="46" rx="10" fill="#58c4fd"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
|
||||||
|
import { defineComponent, computed, onMounted, ref, reactive } from "vue";
|
||||||
|
|
||||||
|
import calcMessagePosition from "./calcPosition";
|
||||||
|
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: "MessageBubble",
|
||||||
|
|
||||||
|
props: {
|
||||||
|
|
||||||
|
// DOM ID of the previous message.
|
||||||
|
// Null if message is first child.
|
||||||
|
anchor: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
|
||||||
|
// Passing in the message contents.
|
||||||
|
message: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
setup(props) {
|
||||||
|
|
||||||
|
// e.g. "translate(20 200)"
|
||||||
|
const position = ref("");
|
||||||
|
let anchorPosition: any;
|
||||||
|
|
||||||
|
// Call to update the position variable.
|
||||||
|
const updatePosition = () => {
|
||||||
|
|
||||||
|
// First get anchor's position.
|
||||||
|
if (props.anchor == "none") {
|
||||||
|
anchorPosition = "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
const m = document.getElementById(props.anchor)
|
||||||
|
if (m) {
|
||||||
|
anchorPosition = m.getBoundingClientRect().bottom
|
||||||
|
}
|
||||||
|
|
||||||
|
position.value = calcMessagePosition(
|
||||||
|
props.message.modifier,
|
||||||
|
100,
|
||||||
|
window.innerWidth,
|
||||||
|
anchorPosition
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Message: ${props.message.id}: \n
|
||||||
|
Anchor: ${props.anchor}: ${anchorPosition} \n
|
||||||
|
Position: ${position.value}`
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assign initial position.
|
||||||
|
updatePosition();
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
|
||||||
|
// We gotta know when the message above us changes position.
|
||||||
|
|
||||||
|
// Add listener for window resize.
|
||||||
|
window.addEventListener("resize", updatePosition);
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
position
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
});
|
||||||
|
</script>
|
||||||
120
src/components/messageView/messageView.vue
Normal file
120
src/components/messageView/messageView.vue
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
<!-- components/messageView/messageView.vue -->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="500"
|
||||||
|
height="500"
|
||||||
|
viewBox="0 0 500 500"
|
||||||
|
>
|
||||||
|
|
||||||
|
<g id="messengerGroup">
|
||||||
|
|
||||||
|
<!-- Messages view bounding box -->
|
||||||
|
<rect id="messagesView" data-name="Rectangle 1496" width="500" height="500" rx="15" fill="#ff8e8e"/>
|
||||||
|
|
||||||
|
<!-- List of messages -->
|
||||||
|
<g id="messages" v-for="(item, index) in renderArr" :key="index">
|
||||||
|
|
||||||
|
<transition
|
||||||
|
appear
|
||||||
|
v-on:before-appear="get"
|
||||||
|
v-on:appear="enter"
|
||||||
|
v-on:after-enter="afterEnter"
|
||||||
|
>
|
||||||
|
<TextBubble :item="item" :key="index" />
|
||||||
|
</transition>
|
||||||
|
|
||||||
|
</g>
|
||||||
|
|
||||||
|
</g>
|
||||||
|
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<!-- <svg
|
||||||
|
version="1.1"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
>
|
||||||
|
<g id="messagesView">
|
||||||
|
<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 { Queue } from "@/modules/queue";
|
||||||
|
import useTransitions from "./viewDetails/transitions";
|
||||||
|
import useScrollView from "@/modules/scrollView";
|
||||||
|
import { defineComponent, reactive, onMounted, onUnmounted, watch } from "vue";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: "MessageView",
|
||||||
|
components: { TextBubble },
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
const renderArr: Message[] = reactive([]);
|
||||||
|
const renderQ = new Queue();
|
||||||
|
|
||||||
|
// 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(() => {
|
||||||
|
window.ipcRenderer.on("render-message", (event, payload) => {
|
||||||
|
const m = payload.message;
|
||||||
|
if (m.content) render(m);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.ipcRenderer.removeAllListeners("render-message");
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
renderArr,
|
||||||
|
beforeEnter,
|
||||||
|
enter,
|
||||||
|
afterEnter,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
#messageView {
|
||||||
|
width: 1000px;
|
||||||
|
height: 100vh;
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
41
src/components/messageView/offsetCalculator.ts
Normal file
41
src/components/messageView/offsetCalculator.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { ref } from "vue";
|
||||||
|
/**
|
||||||
|
* Calculates vertical and horizontal message offsets
|
||||||
|
*/
|
||||||
|
export default function useOffsetCalculator() {
|
||||||
|
const xOffset = ref(0);
|
||||||
|
const yOffset = ref(0);
|
||||||
|
const firstChildMarginTop = 45;
|
||||||
|
let previousMessageHeight = 0;
|
||||||
|
const messagePadding = 15;
|
||||||
|
|
||||||
|
function calculateX(message: HTMLElement| SVGGElement): void {
|
||||||
|
const computedQuery = window.getComputedStyle(message);
|
||||||
|
const matrix = new WebKitCSSMatrix(computedQuery.webkitTransform);
|
||||||
|
xOffset.value = matrix.m41;
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateY(messageHeight: number): void {
|
||||||
|
if (previousMessageHeight === 0) {
|
||||||
|
yOffset.value = firstChildMarginTop;
|
||||||
|
} else {
|
||||||
|
yOffset.value += previousMessageHeight + messagePadding;
|
||||||
|
}
|
||||||
|
previousMessageHeight = messageHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
const calculateMessageOffsets = (msg: HTMLElement): { x: number; y: number } => {
|
||||||
|
const msgRect = msg.getBoundingClientRect();
|
||||||
|
calculateX(msg);
|
||||||
|
calculateY(msgRect.height);
|
||||||
|
const offsets = {
|
||||||
|
x: xOffset.value as number,
|
||||||
|
y: yOffset.value as number
|
||||||
|
};
|
||||||
|
return offsets;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
calculateMessageOffsets,
|
||||||
|
}
|
||||||
|
}
|
||||||
79
src/components/messageView/textBubble/computed.ts
Normal file
79
src/components/messageView/textBubble/computed.ts
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
import { computed, onMounted, ref } from "vue";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* single message svg item computed properties
|
||||||
|
*/
|
||||||
|
export default function useComputedBubble(m: string, id: string) {
|
||||||
|
const yMargin = 0;
|
||||||
|
const messageMargin = 20;
|
||||||
|
const messageWidth = ref(0);
|
||||||
|
|
||||||
|
|
||||||
|
// const textFill = computed(() => {
|
||||||
|
// switch (m) {
|
||||||
|
// case "sf":
|
||||||
|
// return "#fff";
|
||||||
|
// default:
|
||||||
|
// return "#000";
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
// const bubbleFill = computed(() => {
|
||||||
|
// switch (m) {
|
||||||
|
// case "sf":
|
||||||
|
// return "#61b4f4";
|
||||||
|
// default:
|
||||||
|
// return "#fff";
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
// const setMessageWidthRef = () => {
|
||||||
|
// const msg = document.getElementById(id);
|
||||||
|
|
||||||
|
// if (msg) {
|
||||||
|
// messageWidth.value = msg.getBoundingClientRect().width;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// }
|
||||||
|
|
||||||
|
const calculateXoffset = (w: number) => {
|
||||||
|
let offset = messageMargin;
|
||||||
|
|
||||||
|
// If self message, render on righthand side.
|
||||||
|
if (m == "sf") {
|
||||||
|
offset = w - messageMargin - 232
|
||||||
|
|
||||||
|
// Else, render on left.
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`${m} : ${offset}`);
|
||||||
|
return offset
|
||||||
|
}
|
||||||
|
|
||||||
|
const calculateYoffset = (h: number) => {
|
||||||
|
|
||||||
|
// Bubble will render a little bit below the screen.
|
||||||
|
return h + yMargin
|
||||||
|
}
|
||||||
|
|
||||||
|
// Where the textBubble is going to initially render.
|
||||||
|
const startingOffset = computed(() => {
|
||||||
|
|
||||||
|
// Get current dimensions of the messenger.
|
||||||
|
const parentView = document.getElementById('messagesView');
|
||||||
|
|
||||||
|
if (parentView) {
|
||||||
|
const dimensions = parentView.getBoundingClientRect();
|
||||||
|
|
||||||
|
const X = calculateXoffset(dimensions.width);
|
||||||
|
const Y = calculateYoffset(dimensions.height);
|
||||||
|
|
||||||
|
return `translate(${X} ${Y})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
startingOffset
|
||||||
|
};
|
||||||
|
}
|
||||||
50
src/components/messageView/textBubble/drawer.ts
Normal file
50
src/components/messageView/textBubble/drawer.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
import { ref, onMounted } from "vue";
|
||||||
|
import { Ref } from "@/types/vueRef/index";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* handles visualization of single message svg items
|
||||||
|
*/
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
116
src/components/messageView/textBubble/textBubble.vue
Normal file
116
src/components/messageView/textBubble/textBubble.vue
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
<template>
|
||||||
|
<g
|
||||||
|
:id="item.id"
|
||||||
|
:class="item.modifier"
|
||||||
|
:transform="startingOffset"
|
||||||
|
>
|
||||||
|
<rect
|
||||||
|
ref="messageRect"
|
||||||
|
x="30"
|
||||||
|
y="10"
|
||||||
|
width="196"
|
||||||
|
height="63"
|
||||||
|
rx="10"
|
||||||
|
:fill="bubbleFill"
|
||||||
|
/>
|
||||||
|
<text
|
||||||
|
font-size="14"
|
||||||
|
font-family="SF Pro Text"
|
||||||
|
ref="messageText"
|
||||||
|
:fill="textFill">
|
||||||
|
|
||||||
|
<tspan dy="16" x="0" v-for="(item, index) in textArr" :key="index">
|
||||||
|
{{ item }}
|
||||||
|
</tspan>
|
||||||
|
|
||||||
|
</text>
|
||||||
|
|
||||||
|
<g :transform="signatureTranslate">
|
||||||
|
<text
|
||||||
|
transform="translate(0, 5)"
|
||||||
|
fill="#000"
|
||||||
|
font-size="10"
|
||||||
|
font-family="SFCompactDisplay-Bold, SF Compact Display"
|
||||||
|
font-weight="700"
|
||||||
|
>
|
||||||
|
|
||||||
|
<tspan xml:space="preserve">
|
||||||
|
{{ item.context }}
|
||||||
|
</tspan>
|
||||||
|
|
||||||
|
</text>
|
||||||
|
|
||||||
|
<text
|
||||||
|
transform="translate(0, 20)"
|
||||||
|
fill="#6f6f6f"
|
||||||
|
font-size="9"
|
||||||
|
font-family="SFCompactDisplay, SF Compact Display"
|
||||||
|
>
|
||||||
|
|
||||||
|
<tspan v-if="item.subContext">
|
||||||
|
{{item.subContext}}
|
||||||
|
</tspan>
|
||||||
|
|
||||||
|
</text>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
|
||||||
|
import useBubbleDrawer from "./drawer";
|
||||||
|
import useComputedBubble from "./computed";
|
||||||
|
|
||||||
|
import useTextWrap from "@/modules/textWrap";
|
||||||
|
|
||||||
|
import { defineComponent, onMounted, ref, reactive } from "vue";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: "TextBubble",
|
||||||
|
props: {
|
||||||
|
item: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
let textArr: string[] = reactive([]);
|
||||||
|
|
||||||
|
const msgID = ref(props.item.id);
|
||||||
|
const messageRect = ref(`message${msgID.value}`);
|
||||||
|
const messageText = ref(`text${msgID.value}`);
|
||||||
|
|
||||||
|
// Visual attribues of bubble.
|
||||||
|
const { textFill, bubbleFill, startingOffset } = useComputedBubble(
|
||||||
|
props.item.modifier,
|
||||||
|
props.item.id
|
||||||
|
);
|
||||||
|
|
||||||
|
const { textWrap } = useTextWrap();
|
||||||
|
const { drawMessage, signatureTranslate } = useBubbleDrawer({
|
||||||
|
mr: messageRect,
|
||||||
|
tr: messageText,
|
||||||
|
});
|
||||||
|
|
||||||
|
// wraps message text before render
|
||||||
|
textArr = textWrap({
|
||||||
|
s: props.item.content,
|
||||||
|
w: 32,
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
drawMessage();
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
textFill,
|
||||||
|
bubbleFill,
|
||||||
|
startingOffset,
|
||||||
|
textArr,
|
||||||
|
signatureTranslate,
|
||||||
|
messageText,
|
||||||
|
messageRect,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
100
src/components/messageView/transitions.ts
Normal file
100
src/components/messageView/transitions.ts
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
/**
|
||||||
|
* Do stuff before, on, and after message enters the view.
|
||||||
|
* Rendering Process consists of the execution of the above
|
||||||
|
* mentioned callbacks followed by the shift or stack animation.
|
||||||
|
* The animation called depends on the height of the message view
|
||||||
|
* relative to the window height. Additionally, the animation will
|
||||||
|
* always be called on the after enter callback, after all the neccesary
|
||||||
|
* calculations have been completed. While all of this is hapenning,
|
||||||
|
* the rendering state is updated throughout the process.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { VueTransitionCallback } from '@/types/vue3/vueTransitionCallback/index';
|
||||||
|
import useMessageViewAnims from "./anime";
|
||||||
|
import useOffsetCalculator from './offsetCalculator';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
const wH = 500; // window Height
|
||||||
|
const vP = 10; // view padding
|
||||||
|
const mP = 15; // message padding
|
||||||
|
let mH: number; // message Height
|
||||||
|
let vH: number; // view Height
|
||||||
|
let vB: number; // view Bottom Bound
|
||||||
|
let message: HTMLElement | null;
|
||||||
|
let messageOffset = { x: 0, y: 0 };
|
||||||
|
let shift = false;
|
||||||
|
let beforeEnterVH: number;
|
||||||
|
let messageView: SVGGElement;
|
||||||
|
|
||||||
|
export default function useTransitions() {
|
||||||
|
const { calculateMessageOffsets } = useOffsetCalculator();
|
||||||
|
const { shiftAnimate, stackAnimate } = useMessageViewAnims();
|
||||||
|
const rendering = ref(false);
|
||||||
|
|
||||||
|
const animateMessage = async (el: SVGGElement) => {
|
||||||
|
|
||||||
|
// Where the message gets translated to.
|
||||||
|
const elTransform = `translate(${messageOffset.x} ${messageOffset.y})`;
|
||||||
|
|
||||||
|
const viewTransform = `translate(0 ${-vB})`;
|
||||||
|
|
||||||
|
// Shift of view height > window height.
|
||||||
|
if (shift) {
|
||||||
|
await shiftAnimate(el, elTransform, messageView, viewTransform);
|
||||||
|
} else {
|
||||||
|
await stackAnimate(el, elTransform)
|
||||||
|
}
|
||||||
|
rendering.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// const beforeEnter: VueTransitionCallback = () => {
|
||||||
|
// const parentView = document.getElementById('messagesView');
|
||||||
|
|
||||||
|
// if (parentView) {
|
||||||
|
// const dimensions = parentView.getBoundingClientRect();
|
||||||
|
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Get height of messageView.
|
||||||
|
const beforeEnter: VueTransitionCallback = () => {
|
||||||
|
rendering.value = true;
|
||||||
|
// query DOM for messageView element
|
||||||
|
const messageViewQuery = document.querySelectorAll("#messageView");
|
||||||
|
messageView = messageViewQuery[0] as SVGGElement;
|
||||||
|
if (messageViewQuery) {
|
||||||
|
beforeEnterVH = messageView.getBBox().height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const enter: VueTransitionCallback = (el) => {
|
||||||
|
// query DOM for latest message
|
||||||
|
if (el) {
|
||||||
|
message = document.getElementById(el.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const afterEnter: VueTransitionCallback = (el) => {
|
||||||
|
|
||||||
|
if (message) messageOffset = calculateMessageOffsets(message);
|
||||||
|
|
||||||
|
if (el) {
|
||||||
|
// calculate message & view heights, as well as view bottom bound
|
||||||
|
mH = el.getBoundingClientRect().height;
|
||||||
|
vH = mH + beforeEnterVH + mP;
|
||||||
|
vB = Math.abs(vH - wH) + vP;
|
||||||
|
|
||||||
|
shift = vH > wH ? true : false;
|
||||||
|
animateMessage(el);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
beforeEnter,
|
||||||
|
enter,
|
||||||
|
afterEnter,
|
||||||
|
rendering
|
||||||
|
};
|
||||||
|
}
|
||||||
65
src/components/taInput/audioDetails/audio.vue
Normal file
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
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
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
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
|
||||||
|
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
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
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
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
|
||||||
|
}
|
||||||
|
}
|
||||||
81
src/components/textBubble/bubbleDetails/computed.ts
Normal file
81
src/components/textBubble/bubbleDetails/computed.ts
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
import { computed, onMounted, ref } from "vue";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* single message svg item computed properties
|
||||||
|
*/
|
||||||
|
export default function useComputedBubble(m: string, id: string) {
|
||||||
|
const x = ref(0)
|
||||||
|
const paddingLeft = 30;
|
||||||
|
|
||||||
|
let winW = 350;
|
||||||
|
const messageWidth = ref(0);
|
||||||
|
const messageMarginTop = 75;
|
||||||
|
|
||||||
|
const textFill = computed(() => {
|
||||||
|
switch (m) {
|
||||||
|
case "sf":
|
||||||
|
return "#fff";
|
||||||
|
default:
|
||||||
|
return "#000";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const bubbleFill = computed(() => {
|
||||||
|
switch (m) {
|
||||||
|
case "sf":
|
||||||
|
return "#61b4f4";
|
||||||
|
default:
|
||||||
|
return "#fff";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const updatewinW = computed(() => {
|
||||||
|
const parentView = document.getElementById('messageView');
|
||||||
|
if (parentView) {
|
||||||
|
winW = parentView.getBoundingClientRect().width;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const setMessageWidthRef = async () => {
|
||||||
|
const msg = await document.getElementById(id);
|
||||||
|
if (msg) {
|
||||||
|
messageWidth.value = msg.getBoundingClientRect().width;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const selfMessageMarginRight = 2.5;
|
||||||
|
const calculateXoffset = async () => {
|
||||||
|
switch (m) {
|
||||||
|
case "sf":
|
||||||
|
x.value = winW - messageWidth.value - selfMessageMarginRight;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
x.value = paddingLeft;
|
||||||
|
// to center a message
|
||||||
|
// x = messageMargin + (winW -messageWidth) / 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const startingOffset = computed(() => {
|
||||||
|
const parentView = document.getElementById('messageView');
|
||||||
|
if (parentView) {
|
||||||
|
const rect = parentView.getBoundingClientRect();
|
||||||
|
let Y = 600;
|
||||||
|
if (rect.height > 500) {
|
||||||
|
Y = rect.height + messageMarginTop;
|
||||||
|
}
|
||||||
|
return `translate(${x.value} ${Y})`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await setMessageWidthRef();
|
||||||
|
calculateXoffset();
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
textFill,
|
||||||
|
bubbleFill,
|
||||||
|
startingOffset
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
const { autoUpdater } = require('electron-updater');
|
|
||||||
|
|
||||||
let win: boolean;
|
|
||||||
|
|
||||||
// Listen for window creation.
|
|
||||||
backgroundMitt.on('window-active', (state: boolean) => {
|
|
||||||
win = state;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Auto updating.
|
|
||||||
autoUpdater.requestHeaders = { 'PRIVATE-TOKEN': 'mvvgWYwWnot4bisiQMh_' }
|
|
||||||
|
|
||||||
autoUpdater.on('update-available', (info: any) => {
|
|
||||||
console.log(`Update available: ${info.version}`)
|
|
||||||
})
|
|
||||||
|
|
||||||
autoUpdater.on('update-downloaded', (info: any) => {
|
|
||||||
|
|
||||||
const updateDialog = {
|
|
||||||
type: 'info',
|
|
||||||
buttons: ['Restart', 'Later'],
|
|
||||||
title: 'Application Update',
|
|
||||||
message: info.version,
|
|
||||||
detail: 'A new version has been downloaded. Restart the application to apply the updates.'
|
|
||||||
}
|
|
||||||
|
|
||||||
dialog.showMessageBox(updateDialog).then((returnValue) => {
|
|
||||||
if (returnValue.response === 0) autoUpdater.quitAndInstall()
|
|
||||||
})
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
|
|
||||||
// Backend emitter
|
|
||||||
//
|
|
||||||
const EventEmitter = require('events');
|
|
||||||
class BackgroundMitt extends EventEmitter { }
|
|
||||||
|
|
||||||
export const backgroundMitt = new BackgroundMitt();
|
|
||||||
|
|
||||||
export const ipcEmit = <T>(channel: string, payload: T) => {
|
|
||||||
backgroundMitt.emit('ipc-renderer', {
|
|
||||||
channel,
|
|
||||||
payload
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,51 +0,0 @@
|
||||||
|
|
||||||
import axios, { AxiosRequestConfig } from 'axios';
|
|
||||||
import {config} from "@/config";
|
|
||||||
|
|
||||||
const baseURL = config.BUSINESS_URL + config.BUSINESS_PREFIX;
|
|
||||||
|
|
||||||
interface Request {
|
|
||||||
endpoint: string;
|
|
||||||
query?: Record<string, any>;
|
|
||||||
config?: Record<string, any>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const makeQuery = (reqQuery: Record<string, any>) => {
|
|
||||||
|
|
||||||
let result = '';
|
|
||||||
|
|
||||||
result = '?' + Object.entries(reqQuery)
|
|
||||||
.map(([ key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
|
|
||||||
.join('&')
|
|
||||||
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
export default function useHttp() {
|
|
||||||
|
|
||||||
const api = axios.create({
|
|
||||||
baseURL,
|
|
||||||
withCredentials: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
const post = async (endpoint: string, payload?: Record<string, any>): Promise<any> => (
|
|
||||||
await api.post(endpoint, payload)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
const get = async (req: Request) => {
|
|
||||||
|
|
||||||
if (req.query) {
|
|
||||||
req.endpoint += makeQuery(req.query);
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await api.get(req.endpoint, req.config);
|
|
||||||
return res;
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
get, post
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,82 +0,0 @@
|
||||||
|
|
||||||
import { ipcMain, IpcMainInvokeEvent, IpcMainEvent } from "electron";
|
|
||||||
|
|
||||||
export class IpcHandler<InputParam, ReturnType> implements IIpcHandler<InputParam, ReturnType> {
|
|
||||||
|
|
||||||
readonly channel: string;
|
|
||||||
|
|
||||||
readonly _handlerCallback: IpcHandlerCallback<InputParam, ReturnType>;
|
|
||||||
|
|
||||||
constructor(options: {
|
|
||||||
channel: string;
|
|
||||||
handlerCallback: IpcHandlerCallback<InputParam, ReturnType>;
|
|
||||||
}) {
|
|
||||||
this.channel = options.channel;
|
|
||||||
this._handlerCallback = options.handlerCallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
handle() {
|
|
||||||
this.remove();
|
|
||||||
ipcMain.handle(this.channel, this._onInvoke);
|
|
||||||
}
|
|
||||||
|
|
||||||
remove() {
|
|
||||||
ipcMain.removeHandler(this.channel);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _onInvoke = (_e: IpcMainInvokeEvent, payload?: string | null): Promise<Error | ReturnType> => {
|
|
||||||
|
|
||||||
return new Promise(async (resolve, reject) => {
|
|
||||||
|
|
||||||
console.log(`[IPC] Handle:${this.channel}`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
|
|
||||||
const params = payload ? JSON.parse(payload) : null;
|
|
||||||
|
|
||||||
const res = await this._handlerCallback(params);
|
|
||||||
|
|
||||||
resolve(res as unknown as ReturnType);
|
|
||||||
|
|
||||||
} catch(e) {
|
|
||||||
console.log(`[IPC] Error:${this.channel}`);
|
|
||||||
reject(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export class IpcListener<InputParam> implements IIpcListener<InputParam> {
|
|
||||||
|
|
||||||
readonly channel: string;
|
|
||||||
|
|
||||||
readonly _listenerCallback: IpcListenerCallback<InputParam>;
|
|
||||||
|
|
||||||
constructor(options: {
|
|
||||||
channel: string;
|
|
||||||
listenerCallback: IpcListenerCallback<InputParam>;
|
|
||||||
}) {
|
|
||||||
this.channel = options.channel;
|
|
||||||
this._listenerCallback = options.listenerCallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
listen() {
|
|
||||||
this.remove();
|
|
||||||
ipcMain.on(this.channel, this._onPost);
|
|
||||||
}
|
|
||||||
|
|
||||||
remove() {
|
|
||||||
ipcMain.removeAllListeners(this.channel);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _onPost = (_e: IpcMainEvent, payload?: string | null): void => {
|
|
||||||
|
|
||||||
console.log(`[IPC] Post: ${this.channel}`);
|
|
||||||
|
|
||||||
const params = payload ? JSON.parse(payload) : null;
|
|
||||||
this._listenerCallback(params);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export default class Canvas {
|
|
||||||
|
|
||||||
messages: Message[];
|
|
||||||
|
|
||||||
/* seed canvas with messages on init */
|
|
||||||
constructor(messages: Message[]) {
|
|
||||||
this.messages = messages;
|
|
||||||
ipcEmit("seed-view", this.messages);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* add a new message to the canvas */
|
|
||||||
add(message: Message) {
|
|
||||||
this.messages.push(message);
|
|
||||||
ipcEmit("update-view", message);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* update an existing message */
|
|
||||||
update(message: Message) {
|
|
||||||
|
|
||||||
/* get the target message */
|
|
||||||
let target_message = this.messages.filter((m: Message) => {
|
|
||||||
return m.uid = message.uid;
|
|
||||||
})[0];
|
|
||||||
|
|
||||||
/* replace the target message */
|
|
||||||
if (target_message) {
|
|
||||||
target_message = message;
|
|
||||||
ipcEmit("update-view", message);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
|
|
||||||
import {config} from "@/config";
|
|
||||||
import fs from 'fs';
|
|
||||||
|
|
||||||
export const saveToJson = (fileName: string, data: any) => {
|
|
||||||
|
|
||||||
fs.writeFile(config.configPath + fileName, JSON.stringify(data), (err) => {
|
|
||||||
if (err) {
|
|
||||||
console.log("Error when saving to json.")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,89 +0,0 @@
|
||||||
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
import WebSocket from 'ws';
|
|
||||||
|
|
||||||
|
|
||||||
const _connectionCheckTimeout = 4000;
|
|
||||||
const _reconnectTimeout = 1000;
|
|
||||||
let _connectionCheckInterval: ReturnType<typeof setTimeout>;
|
|
||||||
|
|
||||||
|
|
||||||
export default function useWebSockets(
|
|
||||||
messageCallback: (message: string) => void,
|
|
||||||
connectionStatusCallback: (alive: boolean) => void,
|
|
||||||
) {
|
|
||||||
|
|
||||||
let socket: WebSocket;
|
|
||||||
|
|
||||||
const send = async (data: Record<string, any>): Promise<boolean> => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
if (socket) {
|
|
||||||
if (socket.readyState === WebSocket.OPEN) {
|
|
||||||
socket.send(JSON.stringify(data));
|
|
||||||
resolve(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
reject(false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const connect = (socketUrl: string, secret: string) => {
|
|
||||||
|
|
||||||
// avoid setting multiple interval;
|
|
||||||
if (_connectionCheckInterval) clearInterval(_connectionCheckInterval);
|
|
||||||
|
|
||||||
/* create a new socket */
|
|
||||||
socket = new WebSocket(socketUrl);
|
|
||||||
|
|
||||||
/* add event listeners */
|
|
||||||
socket.on("open", () => {
|
|
||||||
|
|
||||||
socket.send(JSON.stringify({key: secret}));
|
|
||||||
|
|
||||||
// ping server
|
|
||||||
_connectionCheckInterval = setInterval(() => {
|
|
||||||
|
|
||||||
socket.ping(null, true, (e: Error) => {
|
|
||||||
if (e) {
|
|
||||||
socket.close();
|
|
||||||
connectionStatusCallback(false);
|
|
||||||
setTimeout(() => connect(socketUrl, secret), _reconnectTimeout);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
}, _connectionCheckTimeout);
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("message", (event: WebSocket.MessageEvent) => {
|
|
||||||
console.log("message received", event);
|
|
||||||
messageCallback(event.toString())
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("close", (event: WebSocket.CloseEvent) => {
|
|
||||||
connectionStatusCallback(false);
|
|
||||||
clearInterval(_connectionCheckInterval);
|
|
||||||
if (!event.wasClean) {
|
|
||||||
setTimeout(() => connect(socketUrl, secret), _reconnectTimeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("pong", () => connectionStatusCallback(true));
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
const close = () => {
|
|
||||||
if (socket) {
|
|
||||||
socket.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
connect,
|
|
||||||
send,
|
|
||||||
close
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
|
|
||||||
import { app } from "electron";
|
|
||||||
|
|
||||||
const env = process.env;
|
|
||||||
|
|
||||||
const PLATFORM_PORT = env.PLATFORM_PORT || 8760;
|
|
||||||
const PLATFORM_IP = env.PLATFORM_IP || 'http://127.0.0.1';
|
|
||||||
|
|
||||||
const BUSINESS_PORT = env.BUSINESS_PORT || 3000;
|
|
||||||
const BUSINESS_IP = env.BUSINESS_IP || 'http://127.0.0.1';
|
|
||||||
|
|
||||||
export const config = {
|
|
||||||
PLATFORM_URL: `${PLATFORM_IP}:${PLATFORM_PORT}`,
|
|
||||||
BUSINESS_URL: `${BUSINESS_IP}:${BUSINESS_PORT}`,
|
|
||||||
BUSINESS_PREFIX: '/api',
|
|
||||||
configPath: app.getPath('userData')
|
|
||||||
}
|
|
||||||
52
src/init.ts
52
src/init.ts
|
|
@ -1,52 +0,0 @@
|
||||||
/**
|
|
||||||
* Entry point for Crimata electron app.
|
|
||||||
* "Look on my Works, ye Mighty, and despair!"
|
|
||||||
*/
|
|
||||||
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
import { app, protocol } from "electron";
|
|
||||||
import createWindow from "./window";
|
|
||||||
import main from "./main";
|
|
||||||
import { backgroundMitt } from '@/composables/useEmitter';
|
|
||||||
|
|
||||||
console.log('Starting Crimata electron app.');
|
|
||||||
|
|
||||||
// Scheme must be registered before the app is ready
|
|
||||||
protocol.registerSchemesAsPrivileged([
|
|
||||||
{ scheme: "app", privileges: { secure: true, standard: true } }
|
|
||||||
]);
|
|
||||||
|
|
||||||
const isDev = require('electron-is-dev');
|
|
||||||
|
|
||||||
let win: boolean;
|
|
||||||
|
|
||||||
// Listen for window creation.
|
|
||||||
backgroundMitt.on('window-active', (state: boolean) => {
|
|
||||||
win = state;
|
|
||||||
});
|
|
||||||
|
|
||||||
/* Start main process on ready */
|
|
||||||
app.on("ready", async () => {
|
|
||||||
await main();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Must keep to ensure app doesn't quit on close.
|
|
||||||
app.on("before-quit", async () => {
|
|
||||||
});
|
|
||||||
|
|
||||||
// Must keep to ensure app doesn't quit on close.
|
|
||||||
app.on("window-all-closed", () => {
|
|
||||||
});
|
|
||||||
|
|
||||||
// When user clicks app icon (re-open)
|
|
||||||
app.on("activate", () => {
|
|
||||||
if (!win) createWindow();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Exit cleanly on request from parent process in development mode.
|
|
||||||
if (isDev) {
|
|
||||||
process.on("SIGTERM", () => {
|
|
||||||
app.quit();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
import { accountLogin, accountLogout, accountProfile } from "@/account";
|
|
||||||
import { IpcHandler } from "@/composables/useIpcMain";
|
|
||||||
import { flush } from "@/audio";
|
|
||||||
|
|
||||||
|
|
||||||
const LOGIN_CHANNEL = "invoke-account-login";
|
|
||||||
const LOGOUT_CHANNEL = "invoke-account-logout";
|
|
||||||
const GET_AUDIO_CHANNEL = "invoke-audio-flush";
|
|
||||||
|
|
||||||
export const loginHandler = new IpcHandler({
|
|
||||||
channel: LOGIN_CHANNEL,
|
|
||||||
handlerCallback: accountLogin
|
|
||||||
});
|
|
||||||
|
|
||||||
export const logoutHandler = new IpcHandler({
|
|
||||||
channel: LOGOUT_CHANNEL,
|
|
||||||
handlerCallback: accountLogout
|
|
||||||
});
|
|
||||||
|
|
||||||
export const getAudioHandler = new IpcHandler({
|
|
||||||
channel: GET_AUDIO_CHANNEL,
|
|
||||||
handlerCallback: flush
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
import * as handlers from "./handlers";
|
|
||||||
import * as listeners from "./listeners";
|
|
||||||
|
|
||||||
const ipcHandlers: IPCHandlers = {};
|
|
||||||
const ipcListeners: IPCListeners = {};
|
|
||||||
|
|
||||||
const _initHandlers = (): void => {
|
|
||||||
for (const [key, handler] of Object.entries(handlers)) {
|
|
||||||
if (!(key in ipcHandlers)) {
|
|
||||||
ipcHandlers[key] = handler;
|
|
||||||
handler.handle();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const _initListeners = (): void => {
|
|
||||||
for (const [key, listener] of Object.entries(listeners)) {
|
|
||||||
if (!(key in ipcListeners)) {
|
|
||||||
ipcListeners[key] = listener;
|
|
||||||
listener.listen();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function initIpcMain(): void {
|
|
||||||
_initHandlers();
|
|
||||||
_initListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
|
|
||||||
import { IpcListener } from "@/composables/useIpcMain"
|
|
||||||
import { sendMessage } from '@/session';
|
|
||||||
import { collect } from "@/audio";
|
|
||||||
import { updateAppState } from "@/account";
|
|
||||||
|
|
||||||
const CLIENT_MESSAGE_CHANNEL = "post-session-send"
|
|
||||||
const GET_AUDIO_CHANNEL = "post-audio-collect";
|
|
||||||
const APP_MOUNT_CHANNEL = "post-app-mount";
|
|
||||||
|
|
||||||
export const messageListener = new IpcListener<Message>({
|
|
||||||
channel: CLIENT_MESSAGE_CHANNEL,
|
|
||||||
listenerCallback: sendMessage
|
|
||||||
});
|
|
||||||
|
|
||||||
export const audioChunkListener = new IpcListener<ArrayBuffer>({
|
|
||||||
channel: GET_AUDIO_CHANNEL,
|
|
||||||
listenerCallback: collect
|
|
||||||
});
|
|
||||||
|
|
||||||
export const appMountListener = new IpcListener<null>({
|
|
||||||
channel: APP_MOUNT_CHANNEL,
|
|
||||||
listenerCallback: updateAppState
|
|
||||||
});
|
|
||||||
47
src/main.ts
47
src/main.ts
|
|
@ -1,39 +1,18 @@
|
||||||
/**
|
// src/main.ts
|
||||||
* Where the background logic really begins, gets called by app.onReady().
|
|
||||||
*
|
|
||||||
* Handles authentication. If profile is set, we launch a session, which consis
|
|
||||||
* of opening a connection with the platform, initializing the audio streams.
|
|
||||||
*
|
|
||||||
* The session is primarily an interface between the frontend and the platform,
|
|
||||||
* relaying messages from one to the other.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
import initIpcMain from "@/ipc/index";
|
import App from "./App.vue";
|
||||||
import { accountAuth, updateAppState } from "./account";
|
import router from "./router";
|
||||||
import { launchSession } from "./session";
|
|
||||||
import createWindow from "./window";
|
|
||||||
|
|
||||||
let authState: AuthState | null;
|
import mitt from "mitt";
|
||||||
|
import anime from "animejs";
|
||||||
|
import { createApp } from "vue";
|
||||||
|
|
||||||
export default async function main() {
|
// Handle events.
|
||||||
|
const emitter = mitt();
|
||||||
|
|
||||||
/* initiate controls for frontend to use when needed */
|
const app = createApp(App)
|
||||||
initIpcMain();
|
|
||||||
|
|
||||||
/* launch browser window */
|
app.use(router)
|
||||||
await createWindow();
|
app.provide("animejs", anime)
|
||||||
|
app.provide("mitt", emitter)
|
||||||
try {
|
app.mount("#app");
|
||||||
authState = await accountAuth() as AuthState;
|
|
||||||
} catch(e) {
|
|
||||||
console.log('AUTH:', e);
|
|
||||||
authState = null;
|
|
||||||
} finally {
|
|
||||||
if (authState) {
|
|
||||||
launchSession(authState.token as string);
|
|
||||||
}
|
|
||||||
updateAppState();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
|
||||||
57
src/modules/auth.ts
Normal file
57
src/modules/auth.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
import { reactive, toRefs } from 'vue';
|
||||||
|
import { useIpc } from './ipc';
|
||||||
|
|
||||||
|
interface AuthState {
|
||||||
|
accessToken: string | null;
|
||||||
|
error?: Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = reactive<AuthState>({
|
||||||
|
accessToken: null,
|
||||||
|
error: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
const AUTH_KEY = 'crimata_token';
|
||||||
|
|
||||||
|
const token = window.localStorage.getItem(AUTH_KEY);
|
||||||
|
|
||||||
|
const authToken = async () => {
|
||||||
|
const { invoke } = useIpc();
|
||||||
|
try {
|
||||||
|
state.accessToken = await invoke('auth-session', token);
|
||||||
|
}
|
||||||
|
catch(e) {
|
||||||
|
state.error = e;
|
||||||
|
console.log('ERROR: failed authentication');
|
||||||
|
window.localStorage.removeItem(AUTH_KEY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// authenticate on auth event
|
||||||
|
window.ipcRenderer.on("auth", async (_event, _arg) => {
|
||||||
|
await authToken();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
state.accessToken = token;
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
23
src/modules/ipc.ts
Normal file
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
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
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
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
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
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
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
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
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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -25,12 +25,8 @@ process.once("loaded", () => {
|
||||||
ipcRenderer.send("update-recorder", message.content);
|
ipcRenderer.send("update-recorder", message.content);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message.endpoint === "client-message") {
|
if (message.endpoint === "message") {
|
||||||
ipcRenderer.send("client-message", message.content);
|
ipcRenderer.send("message", message.content);
|
||||||
}
|
|
||||||
|
|
||||||
if (message.endpoint === "logout") {
|
|
||||||
ipcRenderer.send("logout", message.content);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
@ -1,76 +0,0 @@
|
||||||
<template>
|
|
||||||
<!-- Only render when profile has been set -->
|
|
||||||
<main id="app" v-if="authComplete">
|
|
||||||
|
|
||||||
<Header />
|
|
||||||
|
|
||||||
<Messenger
|
|
||||||
v-if="profile"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Main Components
|
|
||||||
-->
|
|
||||||
<Login v-else />
|
|
||||||
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<!-- Show spash screen if ready is false -->
|
|
||||||
<Splash v-else />
|
|
||||||
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
|
|
||||||
import { defineComponent, onMounted } from "vue";
|
|
||||||
import { IpcRendererEvent } from "electron";
|
|
||||||
|
|
||||||
import Splash from "@/render/components/splash.vue";
|
|
||||||
import Messenger from "@/render/components/messenger.vue";
|
|
||||||
import Login from "@/render/components/login.vue";
|
|
||||||
import Header from "@/render/components/header.vue";
|
|
||||||
|
|
||||||
import { profile, authComplete } from "@/render/composables/useProfile";
|
|
||||||
import { postAppMount } from "@/render/ipc";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
|
|
||||||
components: {
|
|
||||||
Splash,
|
|
||||||
Messenger,
|
|
||||||
Login,
|
|
||||||
Header
|
|
||||||
},
|
|
||||||
|
|
||||||
setup() {
|
|
||||||
|
|
||||||
onMounted(() => postAppMount());
|
|
||||||
|
|
||||||
return {
|
|
||||||
authComplete,
|
|
||||||
profile
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
|
|
||||||
html, body {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#app {
|
|
||||||
font-family: "SF Pro Text";
|
|
||||||
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
|
|
||||||
height: 100vh;
|
|
||||||
width: 100vw;
|
|
||||||
|
|
||||||
border-radius: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,331 +0,0 @@
|
||||||
<template>
|
|
||||||
<div :class="`${type}-message`">
|
|
||||||
|
|
||||||
<!-- message bubble -->
|
|
||||||
<div :class="`${type}-${child}-bubble`">
|
|
||||||
<div class="notification-dot"/>
|
|
||||||
<div class="error-dot"/>
|
|
||||||
{{ text }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- message context -->
|
|
||||||
<span class="context">
|
|
||||||
<div :class="`${type}-icon`"/>
|
|
||||||
{{ context }}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang='ts'>
|
|
||||||
|
|
||||||
import { defineComponent, ref, onMounted } from 'vue';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: "Bubble",
|
|
||||||
|
|
||||||
props: ["text", "context", "type", "position"],
|
|
||||||
|
|
||||||
setup() {
|
|
||||||
|
|
||||||
const seen = ref(false);
|
|
||||||
/* initialize seen state */
|
|
||||||
if (document.visibilityState === "visible") {
|
|
||||||
seen.value = true;
|
|
||||||
} else seen.value = false;
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
|
|
||||||
if(seen.value)
|
|
||||||
document.addEventListener("visibilitychange", () => {
|
|
||||||
seen.value = true
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
seen
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
|
|
||||||
.message {
|
|
||||||
width: 100vw;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
padding-top: 9px;
|
|
||||||
padding-bottom: 9px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message:first-child {
|
|
||||||
margin-top: 55px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message:last-child {
|
|
||||||
margin-bottom: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sf-message {
|
|
||||||
@extend .message;
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ai-message, .fr-message {
|
|
||||||
@extend .message;
|
|
||||||
justify-content: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bubble {
|
|
||||||
position: relative;
|
|
||||||
max-width: 66vw;
|
|
||||||
font-family: "SF Pro Text";
|
|
||||||
font-size: 14px;
|
|
||||||
padding: 10px;
|
|
||||||
border-radius: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sf-bubble {
|
|
||||||
@extend .bubble;
|
|
||||||
background-color: #58c4fd;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.firstChildMessage {
|
|
||||||
padding-bottom: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.middleChildMessage {
|
|
||||||
padding-top: 2px;
|
|
||||||
padding-bottom: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lastChildMessage {
|
|
||||||
padding-top: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#aiMessage {
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
#sfMessage {
|
|
||||||
align-items: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
#frMessage {
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.messageBox {
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
// Animate on render.
|
|
||||||
animation-name: appear;
|
|
||||||
animation-duration: 0.25s;
|
|
||||||
}
|
|
||||||
|
|
||||||
#aiMessageBox {
|
|
||||||
margin-left: 20px;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
#sfMessageBox {
|
|
||||||
margin-right: 20px;
|
|
||||||
align-items: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
#frMessageBox {
|
|
||||||
margin-left: 20px;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bubble {
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
max-width: 66vw;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
font-family: "SF Pro Text";
|
|
||||||
font-size: 14px;
|
|
||||||
|
|
||||||
padding: 10px;
|
|
||||||
|
|
||||||
border-radius: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes appear {
|
|
||||||
10% {
|
|
||||||
transform: scale(0.3);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#aiBubble {
|
|
||||||
background-color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
#sfBubble {
|
|
||||||
background-color: #58c4fd;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
#frBubble {
|
|
||||||
background-color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sf-firstChildBubble {
|
|
||||||
border-bottom-right-radius: 9px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sf-middleChildBubble {
|
|
||||||
border-top-right-radius: 9px;
|
|
||||||
border-bottom-right-radius: 9px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sf-lastChildBubble {
|
|
||||||
border-top-right-radius: 9px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ai-firstChildBubble, .fr-firstChildBubble {
|
|
||||||
border-bottom-left-radius: 9px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ai-middleChildBubble, .fr-middleChildBubble {
|
|
||||||
border-top-left-radius: 9px;
|
|
||||||
border-bottom-left-radius: 9px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ai-lastChildBubble, .fr-lastChildBubble {
|
|
||||||
border-top-left-radius: 9px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notify {
|
|
||||||
position: absolute;
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background-color: #58D9FF;
|
|
||||||
top: -5px;
|
|
||||||
left: -5px;
|
|
||||||
border: 2px solid #EBEBEB;
|
|
||||||
transform: scale(0);
|
|
||||||
|
|
||||||
animation-name: notify-anim;
|
|
||||||
animation-duration: 5s;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes notify-anim {
|
|
||||||
0%, 90% {
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: scale(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.context {
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
font-family: "SF Compact Display";
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: bold;
|
|
||||||
margin-top: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.photo {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
margin-right: 5px;
|
|
||||||
|
|
||||||
width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
|
|
||||||
font-size: 14px;
|
|
||||||
|
|
||||||
background-color: white;
|
|
||||||
border-radius: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply for audio message playback.
|
|
||||||
.playing {
|
|
||||||
animation-name: circle1;
|
|
||||||
animation-duration: 2s;
|
|
||||||
animation-iteration-count: infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes circle1 {
|
|
||||||
0%,
|
|
||||||
100% {
|
|
||||||
box-shadow: 0 0 0 0.4em rgba(195, 195, 195, 0.4), 0 0 0 0.25em rgba(195, 195, 195, 0.15);
|
|
||||||
}
|
|
||||||
25% {
|
|
||||||
box-shadow: 0 0 0 0.15em rgba(195, 195, 195, 0.15), 0 0 0 0.4em rgba(195, 195, 195, 0.3);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
box-shadow: 0 0 0 0.55em rgba(195, 195, 195, 0.55), 0 0 0 0.15em rgba(195, 195, 195, 0.05);
|
|
||||||
}
|
|
||||||
75% {
|
|
||||||
box-shadow: 0 0 0 0.25em rgba(195, 195, 195, 0.25), 0 0 0 0.55em rgba(195, 195, 195, 0.45);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.contentLoader {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.contentLoaderDot {
|
|
||||||
width: 5px;
|
|
||||||
height: 5px;
|
|
||||||
|
|
||||||
margin: 2px;
|
|
||||||
border-radius: 2.5px;
|
|
||||||
background-color: #357CA2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.questionMark {
|
|
||||||
font-weight: 900;
|
|
||||||
color: #357CA2;
|
|
||||||
}
|
|
||||||
|
|
||||||
// .divider {
|
|
||||||
// width: 100vw;
|
|
||||||
// display: flex;
|
|
||||||
// justify-content: center;
|
|
||||||
// align-items: center;
|
|
||||||
|
|
||||||
// font-family: "SF Compact Display";
|
|
||||||
// font-size: 12px;
|
|
||||||
// font-weight: bold;
|
|
||||||
// color: #9B9B9B;
|
|
||||||
|
|
||||||
// margin-bottom: 18px;
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,102 +0,0 @@
|
||||||
import { ref } from "vue";
|
|
||||||
import anime from "animejs";
|
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
|
||||||
|
|
||||||
|
|
||||||
export function animateTextInput () {
|
|
||||||
|
|
||||||
const side = ref("right");
|
|
||||||
|
|
||||||
function show () {
|
|
||||||
const t1 = (side.value === "right") ? 50 : -70;
|
|
||||||
const t2 = (side.value === "right") ? 110 : -130;
|
|
||||||
|
|
||||||
anime({
|
|
||||||
targets: '#textInput',
|
|
||||||
opacity: [0, 1],
|
|
||||||
translateX: [t1, t2],
|
|
||||||
scale: [0.3, 1],
|
|
||||||
duration: 500,
|
|
||||||
easing: 'easeOutExpo',
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function hide () {
|
|
||||||
const t = (side.value === "right") ? 50 : -80;
|
|
||||||
|
|
||||||
anime({
|
|
||||||
targets: '#textInput',
|
|
||||||
opacity: [1, 0],
|
|
||||||
translateX: t,
|
|
||||||
scale: 0.3,
|
|
||||||
duration: 500,
|
|
||||||
easing: 'easeOutExpo',
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function switchSide () {
|
|
||||||
const t = (side.value === "right") ? -130 : 110;
|
|
||||||
|
|
||||||
anime({
|
|
||||||
targets: '#textInput',
|
|
||||||
translateX: t,
|
|
||||||
duration: 500,
|
|
||||||
easing: 'easeOutExpo',
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
side,
|
|
||||||
show,
|
|
||||||
hide,
|
|
||||||
switchSide
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export function animateAudioInput () {
|
|
||||||
|
|
||||||
function show () {
|
|
||||||
anime({
|
|
||||||
targets: '#recIcon',
|
|
||||||
opacity: [0, 0.75],
|
|
||||||
scale: [0.0, 1],
|
|
||||||
duration: 250,
|
|
||||||
easing: 'linear',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function hide () {
|
|
||||||
anime({
|
|
||||||
targets: '#recIcon',
|
|
||||||
opacity: [0.75, 0],
|
|
||||||
scale: [1, 0],
|
|
||||||
duration: 250,
|
|
||||||
easing: 'linear',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
show,
|
|
||||||
hide
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export function newMessage ({
|
|
||||||
text=false,
|
|
||||||
audio=false,
|
|
||||||
context=false,
|
|
||||||
uid=uuidv4()
|
|
||||||
}) {
|
|
||||||
return {
|
|
||||||
text: text,
|
|
||||||
audio: audio,
|
|
||||||
context: context,
|
|
||||||
uid: uid
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,77 +0,0 @@
|
||||||
import { onMounted, onUnmounted, ref, Ref } from "vue";
|
|
||||||
import { postMessage } from "@/render/ipc";
|
|
||||||
import { newMessage, animateAudioInput } from "./helpers";
|
|
||||||
import { invokeReturnAudio, postAudioChunk } from "@/render/ipc";
|
|
||||||
|
|
||||||
export default function useAudioInputController (typing: Ref) {
|
|
||||||
|
|
||||||
const recording = ref(false);
|
|
||||||
let mediaRecorder: MediaRecorder;
|
|
||||||
|
|
||||||
const { show, hide } = animateAudioInput();
|
|
||||||
|
|
||||||
// initialize audio
|
|
||||||
const conf = {audio: true, video: false}
|
|
||||||
navigator.mediaDevices.getUserMedia(conf).then((stream: MediaStream) => {
|
|
||||||
|
|
||||||
const options = {mimeType: 'audio/webm'};
|
|
||||||
mediaRecorder = new MediaRecorder(stream, options);
|
|
||||||
|
|
||||||
// post any mew audio to backend
|
|
||||||
mediaRecorder.addEventListener('dataavailable', (e: BlobEvent) => {
|
|
||||||
e.data.arrayBuffer().then((buff: ArrayBuffer) => {
|
|
||||||
postAudioChunk(buff);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// get audio and post new message to backend
|
|
||||||
mediaRecorder.addEventListener('stop', (_e: Event) => {
|
|
||||||
invokeReturnAudio().then((audio: ArrayBuffer[] | Error) => {
|
|
||||||
console.log(audio);
|
|
||||||
// postMessage(newMessage({audio: audio}));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
// start recording on space bar
|
|
||||||
const record = () => {
|
|
||||||
console.log("INPT:Capturing audio...")
|
|
||||||
mediaRecorder.start();
|
|
||||||
recording.value = true;
|
|
||||||
show()
|
|
||||||
}
|
|
||||||
|
|
||||||
// stop recording and send on release
|
|
||||||
const stop = () => {
|
|
||||||
console.log("INPT:Stopping record.")
|
|
||||||
mediaRecorder.stop();
|
|
||||||
recording.value = false;
|
|
||||||
hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
const onKeyDown = (e: KeyboardEvent) => {
|
|
||||||
if (e.keyCode == 32 && !typing.value) record();
|
|
||||||
}
|
|
||||||
|
|
||||||
const onKeyUp = (e: KeyboardEvent) => {
|
|
||||||
if (e.keyCode == 32 && recording.value) stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
//-----------------------------------------------------------
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
window.addEventListener("keydown", onKeyDown);
|
|
||||||
window.addEventListener("keyup", onKeyUp);
|
|
||||||
});
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
window.removeEventListener("keydown", onKeyDown);
|
|
||||||
window.removeEventListener("keyup", onKeyUp);
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
recording
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,134 +0,0 @@
|
||||||
import { Ref, ref, watch, onMounted, onUnmounted } from "vue";
|
|
||||||
import { postMessage } from "@/render/ipc";
|
|
||||||
import { newMessage, animateTextInput } from "./helpers";
|
|
||||||
|
|
||||||
|
|
||||||
export default function useTextInputController(elementX: Ref) {
|
|
||||||
|
|
||||||
let textInput: HTMLInputElement | null;
|
|
||||||
|
|
||||||
const { side, show, hide, switchSide } = animateTextInput();
|
|
||||||
|
|
||||||
let firstKey = true;
|
|
||||||
const typing = ref(false);
|
|
||||||
|
|
||||||
// Prep inputItem for typing.
|
|
||||||
const prepInput = () => {
|
|
||||||
show()
|
|
||||||
typing.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear and hide inputItem after done typing.
|
|
||||||
const clearInput = () => {
|
|
||||||
|
|
||||||
if (textInput) {
|
|
||||||
textInput.value = "";
|
|
||||||
textInput.blur();
|
|
||||||
}
|
|
||||||
|
|
||||||
hide()
|
|
||||||
firstKey = true;
|
|
||||||
typing.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send a message and clean up after.
|
|
||||||
const sendMessage = () => {
|
|
||||||
if (textInput) {
|
|
||||||
|
|
||||||
// Send it to the backend for processing.
|
|
||||||
const message = newMessage({
|
|
||||||
text: false
|
|
||||||
});
|
|
||||||
|
|
||||||
// postMessage(message);
|
|
||||||
|
|
||||||
clearInput()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keys that are capable of opening the text input (numbers and letters).
|
|
||||||
const isHotKey = (key: number) => {
|
|
||||||
if (key >= 47 && key <= 91) { // a letter
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//---Callbacks-----------------------------------------------
|
|
||||||
|
|
||||||
const onKeyDown = (e: KeyboardEvent) => {
|
|
||||||
const key = e.keyCode;
|
|
||||||
|
|
||||||
if (textInput) {
|
|
||||||
|
|
||||||
// Only runs on firstKey.
|
|
||||||
if (firstKey) {
|
|
||||||
|
|
||||||
if (!isHotKey(key)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
prepInput()
|
|
||||||
}
|
|
||||||
|
|
||||||
textInput.focus();
|
|
||||||
|
|
||||||
// Close input when no text or on ESC.
|
|
||||||
if ((textInput.value == "") && (!firstKey) && (key === 8)) { // backspace
|
|
||||||
clearInput()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (key === 27) { // escape
|
|
||||||
clearInput()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close and send on enter.
|
|
||||||
if (key === 13) {
|
|
||||||
if (textInput.value) {
|
|
||||||
sendMessage()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (firstKey) firstKey = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//-----------------------------------------------------------
|
|
||||||
|
|
||||||
// Watch parent position and update side.
|
|
||||||
watch(elementX, (elementX, _previous) => {
|
|
||||||
const winW = window.innerWidth
|
|
||||||
|
|
||||||
// Logic depends on the side we are on.
|
|
||||||
if (side.value === "right") {
|
|
||||||
if (winW - elementX < 230) {
|
|
||||||
switchSide()
|
|
||||||
side.value = "left"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else {
|
|
||||||
if (winW - elementX > 230) {
|
|
||||||
switchSide()
|
|
||||||
side.value = "right"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
textInput = document.getElementById("textInput") as HTMLInputElement;
|
|
||||||
window.addEventListener("keydown", onKeyDown);
|
|
||||||
})
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
window.removeEventListener("keydown", onKeyDown);
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
typing
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
import { ref } from 'vue';
|
|
||||||
import useScroll from "@/render/composables/useScroll";
|
|
||||||
|
|
||||||
const messagesRef = ref();
|
|
||||||
|
|
||||||
/* seed the canvas with messages */
|
|
||||||
const seedCanvas = (messages: Message[]) => {
|
|
||||||
messagesRef.value = messages;
|
|
||||||
}
|
|
||||||
|
|
||||||
const addMessage = (message: Message) => {
|
|
||||||
messagesRef.value.push(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateMessage = (message: Message) => {
|
|
||||||
|
|
||||||
let target_message = messagesRef.value.filter((m: Message) => {
|
|
||||||
return m.uid = message.uid;
|
|
||||||
})[0];
|
|
||||||
|
|
||||||
if (target_message) {
|
|
||||||
target_message = message;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function useMessages() {
|
|
||||||
|
|
||||||
const { updateScrollRef, adjustScroll } = useScroll("messenger");
|
|
||||||
|
|
||||||
return {
|
|
||||||
messagesRef,
|
|
||||||
seedCanvas,
|
|
||||||
addMessage,
|
|
||||||
updateMessage
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,89 +0,0 @@
|
||||||
<template>
|
|
||||||
<button id="titlebar" />
|
|
||||||
<span id="menu">
|
|
||||||
<button
|
|
||||||
class="menuButton exitButton"
|
|
||||||
@click.prevent="postNavBarExit"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
class="menuButton minimizeButton"
|
|
||||||
@click.prevent="postNavBarMin"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
|
|
||||||
// import { postNavBarExit, postNavBarMin } from "@/render/ipc";
|
|
||||||
import { defineComponent } from "vue";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: "Header",
|
|
||||||
setup() {
|
|
||||||
const postNavBarExit = () => {};
|
|
||||||
const postNavBarMin = () => {};
|
|
||||||
return {
|
|
||||||
postNavBarExit,
|
|
||||||
postNavBarMin
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
#titlebar {
|
|
||||||
position: fixed;
|
|
||||||
|
|
||||||
width: 100vw;
|
|
||||||
height: 54px;
|
|
||||||
|
|
||||||
opacity: 0.75;
|
|
||||||
|
|
||||||
background-color: #EBEBEB;
|
|
||||||
|
|
||||||
-webkit-app-region: drag;
|
|
||||||
|
|
||||||
border: none;
|
|
||||||
outline: none;
|
|
||||||
|
|
||||||
z-index: 1;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
#menu {
|
|
||||||
position: fixed;
|
|
||||||
margin-left: 20px;
|
|
||||||
margin-top: 20px;
|
|
||||||
|
|
||||||
z-index: 2;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.menuButton {
|
|
||||||
border: none;
|
|
||||||
outline:none;
|
|
||||||
min-width: 14px;
|
|
||||||
min-height: 14px;
|
|
||||||
border-radius: 7px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.exitButton {
|
|
||||||
background-color: #FF6157;
|
|
||||||
}
|
|
||||||
|
|
||||||
.exitButton:active {
|
|
||||||
background: #c14645;
|
|
||||||
}
|
|
||||||
|
|
||||||
.minimizeButton {
|
|
||||||
background-color: #FFC12F;
|
|
||||||
margin-left: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.minimizeButton:active {
|
|
||||||
background-color: #c08e38;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,220 +0,0 @@
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
id="inputItem"
|
|
||||||
class="input-item"
|
|
||||||
:class="{ playing: recording }"
|
|
||||||
:style="{ top: `${elementY}px`, left: `${elementX}px` }"
|
|
||||||
>
|
|
||||||
<div>{{ initials }}</div>
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Recording animation on space bar -->
|
|
||||||
<span v-if="recording" class="play"></span>
|
|
||||||
<span v-if="recording" class="pause"></span>
|
|
||||||
|
|
||||||
<!-- Show text input on key-down -->
|
|
||||||
<input
|
|
||||||
id="textInput"
|
|
||||||
type="text"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Show suggestions menu on click -->
|
|
||||||
<!-- <Menu /> -->
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
|
|
||||||
import { defineComponent } from "vue";
|
|
||||||
import draggify from "@/render/composables/useDraggify";
|
|
||||||
|
|
||||||
|
|
||||||
import useTextInputController from
|
|
||||||
"@/render/components/controllers/inputItem.control.text";
|
|
||||||
import useAudioInputController from
|
|
||||||
"@/render/components/controllers/inputItem.control.audio";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: "InputItem",
|
|
||||||
|
|
||||||
props: ["initials"],
|
|
||||||
|
|
||||||
setup() {
|
|
||||||
|
|
||||||
// Default values for position.
|
|
||||||
const xStart = 15;
|
|
||||||
const yStart = window.innerHeight - 200;
|
|
||||||
|
|
||||||
// Calculate position of inputItem on drag.
|
|
||||||
const { elementX, elementY } = draggify("inputItem", xStart, yStart, 15);
|
|
||||||
|
|
||||||
// Controllers for text and audio.
|
|
||||||
const { typing } = useTextInputController(elementX)
|
|
||||||
const { recording } = useAudioInputController(typing)
|
|
||||||
|
|
||||||
return {
|
|
||||||
elementX,
|
|
||||||
elementY,
|
|
||||||
recording
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
|
|
||||||
.input-item {
|
|
||||||
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: 4px solid #C6C6C6;
|
|
||||||
border-radius: 50%;
|
|
||||||
|
|
||||||
// Set opacity here to not affect child.
|
|
||||||
background-color: rgba(235, 235, 235, 0.75);
|
|
||||||
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
box-shadow: 0px 0px 10px 0px rgba(0,0,0,0.15);
|
|
||||||
|
|
||||||
.play,
|
|
||||||
.pause {
|
|
||||||
z-index: 5;
|
|
||||||
&::before,
|
|
||||||
&::after {
|
|
||||||
-webkit-border-radius: 1000px;
|
|
||||||
-moz-border-radius: 1000px;
|
|
||||||
border-radius: 1000px;
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
height: 2.35em;
|
|
||||||
width: 2.35em;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
top: 50%;
|
|
||||||
z-index: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.play::before {
|
|
||||||
box-shadow: 0 0 0 rgba(195, 195, 195, 0);
|
|
||||||
}
|
|
||||||
.pause {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
&.playing {
|
|
||||||
.play {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
.pause {
|
|
||||||
opacity: 1;
|
|
||||||
&::before {
|
|
||||||
-moz-animation: circle1 1.5s infinite ease-in-out;
|
|
||||||
-o-animation: circle1 1.5s infinite ease-in-out;
|
|
||||||
-webkit-animation: circle1 1.5s infinite ease-in-out;
|
|
||||||
animation: circle1 1.5s infinite ease-in-out;
|
|
||||||
}
|
|
||||||
&::after {
|
|
||||||
-moz-animation: circle2 2.2s infinite ease-in-out;
|
|
||||||
-o-animation: circle2 2.2s infinite ease-in-out;
|
|
||||||
-webkit-animation: circle2 2.2s infinite ease-in-out;
|
|
||||||
animation: circle2 2.2s infinite ease-in-out;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.rec-icon {
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background-color: red;
|
|
||||||
transform: translate(14px);
|
|
||||||
|
|
||||||
animation-name: rec;
|
|
||||||
animation-duration: 0.5s;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes rec {
|
|
||||||
from {
|
|
||||||
transform: translate(14px) scale(0.3);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: translate(14px) scale(1.0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes circle1 {
|
|
||||||
0%,
|
|
||||||
100% {
|
|
||||||
box-shadow: 0 0 0 0.4em rgba(195, 195, 195, 0.4);
|
|
||||||
}
|
|
||||||
25% {
|
|
||||||
box-shadow: 0 0 0 0.15em rgba(195, 195, 195, 0.15);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
box-shadow: 0 0 0 0.55em rgba(195, 195, 195, 0.55);
|
|
||||||
}
|
|
||||||
75% {
|
|
||||||
box-shadow: 0 0 0 0.25em rgba(195, 195, 195, 0.25);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes circle2 {
|
|
||||||
0%,
|
|
||||||
100% {
|
|
||||||
box-shadow: 0 0 0 0.25em rgba(195, 195, 195, 0.15);
|
|
||||||
}
|
|
||||||
25% {
|
|
||||||
box-shadow: 0 0 0 0.4em rgba(195, 195, 195, 0.3);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
box-shadow: 0 0 0 0.15em rgba(195, 195, 195, 0.05);
|
|
||||||
}
|
|
||||||
75% {
|
|
||||||
box-shadow: 0 0 0 0.55em rgba(195, 195, 195, 0.45);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#textInput {
|
|
||||||
position: absolute;
|
|
||||||
|
|
||||||
opacity: 0;
|
|
||||||
|
|
||||||
min-width: 150px;
|
|
||||||
height: 16px;
|
|
||||||
|
|
||||||
border-radius: 18px;
|
|
||||||
|
|
||||||
padding: 10px;
|
|
||||||
margin-right: 10px;
|
|
||||||
margin-left: 10px;
|
|
||||||
|
|
||||||
outline: none;
|
|
||||||
border: none;
|
|
||||||
pointer-events: none;
|
|
||||||
|
|
||||||
background-color: white;
|
|
||||||
|
|
||||||
z-index: -1;
|
|
||||||
|
|
||||||
box-shadow: 0px 0px 10px 0px rgba(0,0,0,0.15);
|
|
||||||
|
|
||||||
transform: translateX(50px) scale(0.3);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,82 +0,0 @@
|
||||||
<template>
|
|
||||||
|
|
||||||
<!-- Position fixed items -->
|
|
||||||
<InputItem :initials="profile.initials"/>
|
|
||||||
<Settings />
|
|
||||||
<div id="recIcon" />
|
|
||||||
|
|
||||||
<!-- List of message bubbles. -->
|
|
||||||
<div id="messenger">
|
|
||||||
<Bubble
|
|
||||||
v-for="message in messages"
|
|
||||||
:text="message.content.text"
|
|
||||||
:context="message.context"
|
|
||||||
:key="message[0]"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
|
|
||||||
import { defineComponent, onMounted, onUnmounted } from "vue";
|
|
||||||
import InputItem from "@/render/components/inputItem.vue";
|
|
||||||
import Settings from "@/render/components/settings.vue";
|
|
||||||
import Bubble from "@/render/components/bubble.vue";
|
|
||||||
|
|
||||||
import { profile } from "@/render/composables/useProfile";
|
|
||||||
import { messages } from "@/render/composables/useMessages";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: "Messenger",
|
|
||||||
|
|
||||||
components: {
|
|
||||||
InputItem,
|
|
||||||
Settings,
|
|
||||||
Bubble
|
|
||||||
},
|
|
||||||
|
|
||||||
setup() {
|
|
||||||
return {
|
|
||||||
messages,
|
|
||||||
profile
|
|
||||||
};
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
|
|
||||||
#messenger {
|
|
||||||
width: 100vw;
|
|
||||||
height: 100vh;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
#messenger::-webkit-scrollbar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#recIcon {
|
|
||||||
position: fixed;
|
|
||||||
right: 0;
|
|
||||||
|
|
||||||
opacity: 0;
|
|
||||||
transform: scale(0.0);
|
|
||||||
|
|
||||||
margin-top: 24px;
|
|
||||||
margin-right: 66px;
|
|
||||||
|
|
||||||
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background-color: #FF3B3B;
|
|
||||||
|
|
||||||
z-index: 2;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,164 +0,0 @@
|
||||||
<template>
|
|
||||||
|
|
||||||
<!-- Settings icon. -->
|
|
||||||
<button
|
|
||||||
class="settingsIcon"
|
|
||||||
v-if="toggleSettings == false"
|
|
||||||
@click="onActive"
|
|
||||||
>
|
|
||||||
<div class="settingsButtonDot"></div>
|
|
||||||
<div class="settingsButtonDot"></div>
|
|
||||||
<div class="settingsButtonDot"></div>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- Settings div. -->
|
|
||||||
<div id="settings" v-show="toggleSettings">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
v-if="toggleSettings"
|
|
||||||
class="settingsOption"
|
|
||||||
@click="onLogout"
|
|
||||||
>
|
|
||||||
Logout
|
|
||||||
</button>
|
|
||||||
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
|
|
||||||
import { defineComponent, ref } from "vue";
|
|
||||||
import { clearProfile } from "@/render/composables/useProfile"
|
|
||||||
import { invokeLogout } from "@/render/ipc";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: "Settings",
|
|
||||||
|
|
||||||
setup() {
|
|
||||||
const toggleSettings = ref(false);
|
|
||||||
|
|
||||||
// Listen for escape key to close settings.
|
|
||||||
const onEscape = (e: any) => {
|
|
||||||
if(e.key === "Escape") {
|
|
||||||
toggleSettings.value = false;
|
|
||||||
}
|
|
||||||
window.removeEventListener("keydown", onEscape);
|
|
||||||
}
|
|
||||||
|
|
||||||
// When settings is showing.
|
|
||||||
const onActive = () => {
|
|
||||||
toggleSettings.value = true;
|
|
||||||
window.addEventListener("keydown", onEscape);
|
|
||||||
}
|
|
||||||
|
|
||||||
// We ask server to log us out.
|
|
||||||
const onLogout = async () => {
|
|
||||||
|
|
||||||
console.log("Submitting logout request.");
|
|
||||||
|
|
||||||
try {
|
|
||||||
await invokeLogout();
|
|
||||||
clearProfile();
|
|
||||||
} catch(e) {
|
|
||||||
console.log('error')
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
onActive,
|
|
||||||
toggleSettings,
|
|
||||||
onLogout
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
|
|
||||||
#settings {
|
|
||||||
position: fixed;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
width: 100vw;
|
|
||||||
height: 100vh;
|
|
||||||
|
|
||||||
background-color: #EBEBEB;
|
|
||||||
|
|
||||||
opacity: 0.75;
|
|
||||||
|
|
||||||
z-index: 4;
|
|
||||||
|
|
||||||
animation-name: appear;
|
|
||||||
animation-duration: 0.5s;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes appear {
|
|
||||||
from {
|
|
||||||
opacity: 0
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 0.75
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.settingsIcon {
|
|
||||||
position: fixed;
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settingsIcon:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settingsButtonDot {
|
|
||||||
width: 5px;
|
|
||||||
height: 5px;
|
|
||||||
|
|
||||||
margin: 2px;
|
|
||||||
border-radius: 2.5px;
|
|
||||||
background-color: #9B9B9B;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.settingsOption {
|
|
||||||
font-family: "SF Compact Display";
|
|
||||||
background-color: #B7B7B7;
|
|
||||||
padding: 10px 30px;
|
|
||||||
border-radius: 20px;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: bold;
|
|
||||||
border: none;
|
|
||||||
outline: none;
|
|
||||||
text-decoration: none;
|
|
||||||
position: absolute;
|
|
||||||
left: 50%;
|
|
||||||
top: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
z-index: 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settingsOption:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|
||||||
|
|
@ -1,199 +0,0 @@
|
||||||
import { onMounted, ref, onUnmounted } from "vue";
|
|
||||||
|
|
||||||
//---Dragabble Helper Funcs--------------------------------------
|
|
||||||
|
|
||||||
// Calculate distance to nearest side.
|
|
||||||
function calcSideProximity (elementX: number, winW: number) {
|
|
||||||
|
|
||||||
// Calc right short.
|
|
||||||
let short = winW - elementX - 40;
|
|
||||||
|
|
||||||
// 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: number, percent: number,
|
|
||||||
short: number, win: number) {
|
|
||||||
|
|
||||||
// Is it close to the right/bottom side?
|
|
||||||
if (percent > 0.75) {
|
|
||||||
elementPosition = win - short - 40;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Is it not close to a side?
|
|
||||||
if (percent < 0.75 && percent > 0.25) {
|
|
||||||
elementPosition = win * percent
|
|
||||||
}
|
|
||||||
|
|
||||||
return elementPosition
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function draggify(elementId: string, xStart: number,
|
|
||||||
yStart: number, margin: number) {
|
|
||||||
|
|
||||||
let element: HTMLElement | null;
|
|
||||||
|
|
||||||
// Cords of inputItem.
|
|
||||||
const elementX = ref(0);
|
|
||||||
const elementY = ref(0);
|
|
||||||
|
|
||||||
// Position of inputItem on terms of percentage of window.
|
|
||||||
let percentX: number;
|
|
||||||
let percentY: number;
|
|
||||||
|
|
||||||
// How close inputItem is to closest X or Y side.
|
|
||||||
let xShort: number;
|
|
||||||
let yShort: number;
|
|
||||||
|
|
||||||
// Keep track of window size;
|
|
||||||
let winW = window.innerWidth;
|
|
||||||
let winH = window.innerHeight;
|
|
||||||
|
|
||||||
//---Reposition Anime-----------------------------------------------
|
|
||||||
|
|
||||||
// Move element to target smoothly.
|
|
||||||
const repositionAnime = (xChange: number, yChange: number) => {
|
|
||||||
const xStep = xChange / 6000;
|
|
||||||
const yStep = yChange / 6000;
|
|
||||||
|
|
||||||
for (let i = 1; i <= 6000; i++) {
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
elementX.value += xStep;
|
|
||||||
elementY.value += yStep;
|
|
||||||
}, 16) // 60fps
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//---Event Handlers-----------------------------------------------
|
|
||||||
|
|
||||||
// Update the position of inputItem on mouse dragging.
|
|
||||||
const onMouseMove = (e: any) => {
|
|
||||||
e.preventDefault()
|
|
||||||
element = document.getElementById(elementId);
|
|
||||||
|
|
||||||
if (element) {
|
|
||||||
const inputItemRect = element.getBoundingClientRect();
|
|
||||||
elementX.value = inputItemRect.left + e.movementX;
|
|
||||||
elementY.value = inputItemRect.top + e.movementY;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add an event listener for dragging.
|
|
||||||
const onMouseDown = (_e: any) => {
|
|
||||||
_e.preventDefault()
|
|
||||||
window.addEventListener('mousemove', onMouseMove, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update position references when user is done moving targetEl.
|
|
||||||
const onMouseUp = (_e: any) => {
|
|
||||||
window.removeEventListener('mousemove', onMouseMove, true);
|
|
||||||
|
|
||||||
// See if and calculate reposition.
|
|
||||||
let x = 0; // vector change
|
|
||||||
let y = 0;
|
|
||||||
|
|
||||||
if (elementX.value < 0) {
|
|
||||||
x = (elementX.value - margin)*-1
|
|
||||||
}
|
|
||||||
|
|
||||||
if (elementX.value > winW - 40) {
|
|
||||||
const b = winW - 40 - margin;
|
|
||||||
x = (elementX.value - b)*-1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reposition y
|
|
||||||
if (elementY.value < 0) {
|
|
||||||
y = (elementY.value - margin)*-1
|
|
||||||
}
|
|
||||||
|
|
||||||
if (elementY.value > winH - 40) {
|
|
||||||
const d = winH - 40 - margin;
|
|
||||||
y = (elementY.value - d)*-1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reposition if needed.
|
|
||||||
if (x !== 0 || y !== 0) repositionAnime(x, y);
|
|
||||||
|
|
||||||
xShort = calcSideProximity(elementX.value, winW);
|
|
||||||
yShort = calcSideProximity(elementY.value, winH);
|
|
||||||
|
|
||||||
// Update percentages.
|
|
||||||
percentX = elementX.value / winW;
|
|
||||||
percentY = elementY.value / winH;
|
|
||||||
|
|
||||||
// Save position.
|
|
||||||
const position = {
|
|
||||||
x: elementX.value,
|
|
||||||
y: elementY.value
|
|
||||||
}
|
|
||||||
window.localStorage.setItem("inputItem_position", JSON.stringify(position));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update position of targetEl on windowResize.
|
|
||||||
const onWindowResize = (_e: any) => {
|
|
||||||
|
|
||||||
// Update window dimensions.
|
|
||||||
winW = window.innerWidth;
|
|
||||||
winH = window.innerHeight;
|
|
||||||
|
|
||||||
elementX.value = calcPosition(elementX.value, percentX, xShort, winW);
|
|
||||||
elementY.value = calcPosition(elementY.value, percentY, yShort, winH);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
//---------------------------------------------------------------
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
element = document.getElementById(elementId);
|
|
||||||
|
|
||||||
if (element) {
|
|
||||||
element.addEventListener('mousedown', onMouseDown, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener("mouseup", onMouseUp, false);
|
|
||||||
|
|
||||||
// Initialize the positional references.
|
|
||||||
percentX = elementX.value / window.innerWidth;
|
|
||||||
percentY = elementY.value / window.innerHeight;
|
|
||||||
|
|
||||||
xShort = calcSideProximity(elementX.value, window.innerWidth);
|
|
||||||
yShort = calcSideProximity(elementX.value, window.innerHeight);
|
|
||||||
|
|
||||||
// Then, we can listen for window resize.
|
|
||||||
window.addEventListener('resize', onWindowResize, false);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Try loading initPosition, otherwise set default values
|
|
||||||
let initPosition: any;
|
|
||||||
const rawData = window.localStorage.getItem("inputItem_position")
|
|
||||||
|
|
||||||
if (rawData) {
|
|
||||||
initPosition = JSON.parse(rawData)
|
|
||||||
} else {
|
|
||||||
initPosition = {x: xStart, y: yStart}
|
|
||||||
}
|
|
||||||
|
|
||||||
elementX.value = initPosition.x;
|
|
||||||
elementY.value = initPosition.y;
|
|
||||||
|
|
||||||
// remove event listeners on component dismount.
|
|
||||||
onUnmounted(() => {
|
|
||||||
window.removeEventListener('resize', onWindowResize);
|
|
||||||
window.removeEventListener('mouseup', onMouseUp);
|
|
||||||
if (element) element.removeEventListener('mousedown', onMouseDown);
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
elementX,
|
|
||||||
elementY
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,54 +0,0 @@
|
||||||
|
|
||||||
import { IpcRendererEvent } from "electron";
|
|
||||||
|
|
||||||
export class IpcRendererListener<InputParam> implements IIpcListener<InputParam> {
|
|
||||||
|
|
||||||
readonly channel: string;
|
|
||||||
|
|
||||||
readonly _listenerCallback: IpcListenerCallback<InputParam>;
|
|
||||||
|
|
||||||
constructor(options: {
|
|
||||||
channel: string;
|
|
||||||
listenerCallback: IpcListenerCallback<InputParam>;
|
|
||||||
}) {
|
|
||||||
this.channel = options.channel;
|
|
||||||
this._listenerCallback = options.listenerCallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
listen() {
|
|
||||||
this.remove();
|
|
||||||
window.ipcRenderer.on(this.channel, this._onPost);
|
|
||||||
}
|
|
||||||
|
|
||||||
remove() {
|
|
||||||
window.ipcRenderer.removeAllListeners(this.channel);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _onPost = (_e: IpcRendererEvent, payload: InputParam): void => {
|
|
||||||
console.log(`[IPC] Post: ${this.channel}`);
|
|
||||||
this._listenerCallback(payload);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export default function useIpcRenderer () {
|
|
||||||
|
|
||||||
const invoke = async (endpoint: string, payload: any) => {
|
|
||||||
try {
|
|
||||||
const res = await window.ipcRenderer.invoke(endpoint, payload);
|
|
||||||
return res;
|
|
||||||
} catch (e) {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const post = (endpoint: string, payload: any) => {
|
|
||||||
window.ipcRenderer.send(endpoint, payload);
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
invoke,
|
|
||||||
post,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
// shared
|
|
||||||
import { ref, Ref } from "vue";
|
|
||||||
import useScroll from "@/render/composables/useScroll";
|
|
||||||
|
|
||||||
export const messages: Ref<Array<Message>> = ref([]);
|
|
||||||
|
|
||||||
export const setMessages: IpcListenerCallback<Array<Message>> = (payload) => {
|
|
||||||
messages.value = payload as Array<Message>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const addMessage: IpcListenerCallback<Message> = (payload) => {
|
|
||||||
messages.value.push(payload as Message);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const updateMessage: IpcListenerCallback<Message> = (payload) => {
|
|
||||||
const message = payload as Message;
|
|
||||||
|
|
||||||
let targetMessage = messages.value.filter((m: Message) => {
|
|
||||||
return m.uid = message.uid;
|
|
||||||
})[0];
|
|
||||||
|
|
||||||
if (targetMessage) {
|
|
||||||
targetMessage = message;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
messages,
|
|
||||||
setMessages,
|
|
||||||
addMessage,
|
|
||||||
updateMessage
|
|
||||||
};
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
// shared
|
|
||||||
import { ref } from "vue";
|
|
||||||
|
|
||||||
export const profile = ref();
|
|
||||||
|
|
||||||
export const authComplete = ref(false);
|
|
||||||
|
|
||||||
export const setProfile: IpcListenerCallback<Profile | null> = (payload) => {
|
|
||||||
payload ? profile.value = payload : clearProfile();
|
|
||||||
showRender();
|
|
||||||
};
|
|
||||||
|
|
||||||
export const clearProfile = () => {
|
|
||||||
profile.value = null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const showRender = () => {
|
|
||||||
authComplete.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default {
|
|
||||||
setProfile,
|
|
||||||
clearProfile,
|
|
||||||
profile,
|
|
||||||
showRender,
|
|
||||||
authComplete,
|
|
||||||
};
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
|
|
||||||
|
|
||||||
export default function useScroll(element: string) {
|
|
||||||
|
|
||||||
let isScrolledToBottom: boolean;
|
|
||||||
const view = document.getElementById(element)
|
|
||||||
|
|
||||||
// Update isScrolledToBottom
|
|
||||||
const updateScrollRef = () => {
|
|
||||||
if (view) isScrolledToBottom = view.scrollHeight - view.clientHeight <= view.scrollTop + 1;
|
|
||||||
return isScrolledToBottom;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Adjust scroll after we add content to the messenger.
|
|
||||||
const adjustScroll = () => {
|
|
||||||
if (view) {
|
|
||||||
view.scrollTo({
|
|
||||||
top: view.scrollHeight - view.clientHeight,
|
|
||||||
behavior: 'smooth'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
updateScrollRef,
|
|
||||||
adjustScroll,
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,80 +0,0 @@
|
||||||
|
|
||||||
import useIpc from "@/render/composables/useIpcRend";
|
|
||||||
import * as rendererListeners from "./listeners";
|
|
||||||
|
|
||||||
const { post, invoke } = useIpc();
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* Account and auth related endpoints
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
export const invokeLogin = async (
|
|
||||||
payload: LoginPayload
|
|
||||||
): Promise<Profile | Error> => (
|
|
||||||
await invoke('invoke-account-login', JSON.stringify(payload))
|
|
||||||
);
|
|
||||||
|
|
||||||
export const invokeLogout = async (): Promise<void> => (
|
|
||||||
await invoke("invoke-account-logout", null)
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* Audio endpoints
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const postAudioChunk = (chunk: ArrayBuffer): void => (
|
|
||||||
post("post-audio-collect", chunk)
|
|
||||||
);
|
|
||||||
|
|
||||||
export const invokeReturnAudio = async (): Promise<ArrayBuffer[] | Error> => (
|
|
||||||
await invoke("invoke-audio-flush", null)
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* Crimata Platform (session) endpoints
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const invokeSession = async (cid: string): Promise<Profile | Error> => (
|
|
||||||
await invoke("messenger-init", cid)
|
|
||||||
);
|
|
||||||
|
|
||||||
export const postMessage = (payload: Message): void => (
|
|
||||||
post('post-session-send', payload)
|
|
||||||
);
|
|
||||||
|
|
||||||
export const postAppMount = (): void => (
|
|
||||||
post('post-app-mount', null)
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* Ipc Renderer Listeners
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
let ipcListeners: IPCListeners = {};
|
|
||||||
|
|
||||||
export const initIpcRendererListeners = () => {
|
|
||||||
for (const [key, listener] of Object.entries(rendererListeners)) {
|
|
||||||
if (!(key in ipcListeners)) {
|
|
||||||
ipcListeners[key] = listener;
|
|
||||||
listener.listen();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const removeListeners = () => {
|
|
||||||
for (const [key, listener] of Object.entries(rendererListeners)) {
|
|
||||||
listener.remove();
|
|
||||||
}
|
|
||||||
ipcListeners = {};
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
|
|
||||||
import { IpcRendererListener } from "./composables/useIpcRend"
|
|
||||||
import { setProfile } from "./composables/useProfile";
|
|
||||||
import { setMessages, addMessage, updateMessage } from "./composables/useMessages";
|
|
||||||
|
|
||||||
const SET_PROFILE_CHANNEL = "set-profile";
|
|
||||||
|
|
||||||
const INIT_MESSAGES_CHANNEL = "init-messages";
|
|
||||||
const ADD_MESSAGE_CHANNEL = "add-message";
|
|
||||||
const UPDATE_MESSAGE_CHANNEL = "update-message";
|
|
||||||
|
|
||||||
export const setProfileListener = new IpcRendererListener({
|
|
||||||
channel: SET_PROFILE_CHANNEL,
|
|
||||||
listenerCallback: setProfile
|
|
||||||
});
|
|
||||||
|
|
||||||
export const initMessagesListener = new IpcRendererListener({
|
|
||||||
channel: INIT_MESSAGES_CHANNEL,
|
|
||||||
listenerCallback: setMessages
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
export const addMessagesListener = new IpcRendererListener({
|
|
||||||
channel: ADD_MESSAGE_CHANNEL,
|
|
||||||
listenerCallback: addMessage
|
|
||||||
});
|
|
||||||
|
|
||||||
export const updateMessagesListener = new IpcRendererListener({
|
|
||||||
channel: UPDATE_MESSAGE_CHANNEL,
|
|
||||||
listenerCallback: updateMessage
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
|
|
||||||
// src/main.ts
|
|
||||||
|
|
||||||
import App from "./App.vue";
|
|
||||||
|
|
||||||
import mitt from "mitt";
|
|
||||||
import { createApp } from "vue";
|
|
||||||
|
|
||||||
import { initIpcRendererListeners } from "./ipc"
|
|
||||||
|
|
||||||
|
|
||||||
// Handle ipcMain events.
|
|
||||||
initIpcRendererListeners();
|
|
||||||
|
|
||||||
// Handle events.
|
|
||||||
const emitter = mitt();
|
|
||||||
|
|
||||||
const app = createApp(App);
|
|
||||||
|
|
||||||
app.provide("mitt", emitter);
|
|
||||||
app.mount("#app");
|
|
||||||
64
src/router.ts
Normal file
64
src/router.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
import {
|
||||||
|
createRouter,
|
||||||
|
createWebHistory,
|
||||||
|
createWebHashHistory,
|
||||||
|
RouteRecordRaw
|
||||||
|
} from "vue-router";
|
||||||
|
import Home from "@/views/messenger.vue";
|
||||||
|
import Splash from "@/views/splash.vue";
|
||||||
|
import { useAuth } from '@/modules/auth';
|
||||||
|
|
||||||
|
// Define the routes (/*) for the app here.
|
||||||
|
const routes: Array<RouteRecordRaw> = [
|
||||||
|
{
|
||||||
|
path: "/",
|
||||||
|
name: "home",
|
||||||
|
component: Home,
|
||||||
|
meta: { requiresAuth: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/splash",
|
||||||
|
name: "splash",
|
||||||
|
component: Splash,
|
||||||
|
meta: {
|
||||||
|
requiresAuth: false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/login",
|
||||||
|
name: "login",
|
||||||
|
component: () => import("@/views/login.vue"),
|
||||||
|
meta: { requiresAuth: false },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/register",
|
||||||
|
name: "register",
|
||||||
|
component: () => import("@/views/register.vue"),
|
||||||
|
meta: { requiresAuth: false },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: process.env.IS_ELECTRON ? createWebHashHistory() : createWebHistory(process.env.BASE_URL),
|
||||||
|
routes
|
||||||
|
});
|
||||||
|
|
||||||
|
// route auth check
|
||||||
|
router.beforeEach((to, from, next) => {
|
||||||
|
const { accessToken } = useAuth();
|
||||||
|
|
||||||
|
// Not logged into a guarded route?
|
||||||
|
if (to.meta.requiresAuth && !accessToken.value) {
|
||||||
|
next({ name: 'login' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logged in for an auth route
|
||||||
|
else if ((to.name == 'login' || to.name == 'register') && accessToken.value){
|
||||||
|
next({ name: 'home' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Carry On...
|
||||||
|
else next();
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router;
|
||||||
101
src/session.ts
101
src/session.ts
|
|
@ -1,101 +0,0 @@
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// import useAudio from "@/audio";
|
|
||||||
import { ipcEmit } from "@/composables/useEmitter";
|
|
||||||
import useWebsockets from "./composables/useWebsockets";
|
|
||||||
import {config} from "@/config";
|
|
||||||
|
|
||||||
/* data structure of messages that's tied to the UI */
|
|
||||||
const uiState: any | null = null;
|
|
||||||
|
|
||||||
/* start and stop audio functionality */
|
|
||||||
// const { initAudio, closeAudio } = useAudio();
|
|
||||||
|
|
||||||
const isInitMessage = (message: any): boolean => {
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const deauthenticate = (): void => {
|
|
||||||
console.log('deauthenticating')
|
|
||||||
};
|
|
||||||
|
|
||||||
const isAddMessage = (message: any): boolean => {
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Controls for interfacing with the platform.
|
|
||||||
* Takes an onMessage callback which we define below.
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
const onMessageCallback = (payload: string) => {
|
|
||||||
|
|
||||||
const message = JSON.parse(payload);
|
|
||||||
console.log(typeof message);
|
|
||||||
|
|
||||||
/* if the platform fails to authenticate, we must back down */
|
|
||||||
if (message === "CLOSE_AUTH_FAIL") {
|
|
||||||
deauthenticate();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ipcEmit("add-message", message)
|
|
||||||
return
|
|
||||||
|
|
||||||
/* on init, platform sends state, used to init canvas */
|
|
||||||
if (isInitMessage(message)) {
|
|
||||||
ipcEmit("init-messages", message)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
else if (isAddMessage(message)) {
|
|
||||||
ipcEmit("add-messages", message)
|
|
||||||
}
|
|
||||||
|
|
||||||
else {
|
|
||||||
ipcEmit("update-messages", message)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
const onConnectionStatusCallback = (alive: boolean) => {
|
|
||||||
// console.log('[Session]: Connection Alive: ', alive);
|
|
||||||
// ipcEmit('connection-state', alive);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { connect, send, close } = useWebsockets(
|
|
||||||
onMessageCallback,
|
|
||||||
onConnectionStatusCallback
|
|
||||||
);
|
|
||||||
|
|
||||||
/* send a message to the platform */
|
|
||||||
export function sendMessage<Message>(message: Message): void {
|
|
||||||
|
|
||||||
/* socket send */
|
|
||||||
send(message);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* launch a new session (the main process for authenticated users) */
|
|
||||||
export function launchSession(platformKey: string) {
|
|
||||||
|
|
||||||
/* connect to the platform */
|
|
||||||
connect(config.PLATFORM_URL, platformKey);
|
|
||||||
|
|
||||||
/* initialize the audio streams */
|
|
||||||
// initAudio();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export function endSession() {
|
|
||||||
|
|
||||||
// closeAudio();
|
|
||||||
|
|
||||||
close();
|
|
||||||
|
|
||||||
}
|
|
||||||
31
src/store.ts
31
src/store.ts
|
|
@ -1,31 +0,0 @@
|
||||||
const Store = require('electron-store');
|
|
||||||
|
|
||||||
const schema = {
|
|
||||||
token: {
|
|
||||||
type: 'string',
|
|
||||||
},
|
|
||||||
profile: {}
|
|
||||||
};
|
|
||||||
|
|
||||||
const store = new Store({
|
|
||||||
schema,
|
|
||||||
encryptionKey: "super user test"
|
|
||||||
});
|
|
||||||
|
|
||||||
export const getToken = (): string | undefined => (store.get("token"));
|
|
||||||
|
|
||||||
export const clearToken = (): void => (store.delete("token"));
|
|
||||||
|
|
||||||
export const setToken = (token: string): void => (store.set('token', token));
|
|
||||||
|
|
||||||
export const setProfile = (profile: Profile): Profile => (store.set('profile', profile));
|
|
||||||
|
|
||||||
export const getProfile = (): Profile => (store.get('profile'));
|
|
||||||
|
|
||||||
export const clearProfile = (): void => (store.delete('profile'));
|
|
||||||
|
|
||||||
export const clearStore = (): void => {
|
|
||||||
clearToken();
|
|
||||||
clearProfile();
|
|
||||||
}
|
|
||||||
|
|
||||||
77
src/types.ts
77
src/types.ts
|
|
@ -1,77 +0,0 @@
|
||||||
|
|
||||||
interface Message {
|
|
||||||
text: boolean | string;
|
|
||||||
context: boolean | string;
|
|
||||||
audio: boolean | string;
|
|
||||||
type: 1 | 2 | 3;
|
|
||||||
uid: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ViewMessage extends Message {
|
|
||||||
child: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface WindowState {
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
x: number | null;
|
|
||||||
y: number | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Profile {
|
|
||||||
crimataId: string;
|
|
||||||
alias: string;
|
|
||||||
initials: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface LoginPayload {
|
|
||||||
email: string;
|
|
||||||
password: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AuthState {
|
|
||||||
profile: Profile | null;
|
|
||||||
token: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AccountCredentials {
|
|
||||||
email: string;
|
|
||||||
password: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Electron Ipc
|
|
||||||
*/
|
|
||||||
interface IpcHandlerCallback<I, O> {
|
|
||||||
(payload: I | null): Promise<Error | O>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IpcListenerCallback<T> {
|
|
||||||
(payload: T | null): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IIpcHandler<I, O> {
|
|
||||||
handle(): void;
|
|
||||||
remove(): void;
|
|
||||||
readonly _handlerCallback: IpcHandlerCallback<I, O>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IIpcListener<I> {
|
|
||||||
listen(): void;
|
|
||||||
remove(): void;
|
|
||||||
readonly _listenerCallback: IpcListenerCallback<I>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IPCHandlers {
|
|
||||||
[handler: string]: IIpcHandler<any, any>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IPCListeners {
|
|
||||||
[listener: string]: IIpcListener<any> | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IpcRendererEvent<T> {
|
|
||||||
channel: string;
|
|
||||||
payload: T | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
18
src/types/animejs/index.ts
Normal file
18
src/types/animejs/index.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
|
||||||
|
interface AnimeFuncParams {
|
||||||
|
targets: Element | HTMLElement | SVGGElement | SVGElement;
|
||||||
|
translateY?: number;
|
||||||
|
translateX?: number | any[] | undefined;
|
||||||
|
transform?: string;
|
||||||
|
easing?: string;
|
||||||
|
duration?: number;
|
||||||
|
offset?: number;
|
||||||
|
begin?: () => void;
|
||||||
|
complete?: () => void;
|
||||||
|
stroke?: string;
|
||||||
|
opacity?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default interface AnimeFunc {
|
||||||
|
(params: AnimeFuncParams): void | undefined;
|
||||||
|
}
|
||||||
18
src/types/message/index.ts
Normal file
18
src/types/message/index.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
export interface Message {
|
||||||
|
content: string;
|
||||||
|
avatar: string;
|
||||||
|
context: string;
|
||||||
|
subContext: string;
|
||||||
|
modifier: string;
|
||||||
|
id: string;
|
||||||
|
time: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MessageTransform {
|
||||||
|
initialTranslate: string;
|
||||||
|
finalTranslate: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MessageAnimeFunc {
|
||||||
|
(message: SVGElement, transform: MessageTransform, scrollHeight?: number | undefined, targetList?: NodeList | undefined): void;
|
||||||
|
}
|
||||||
3
src/types/mitt/index.ts
Normal file
3
src/types/mitt/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
import { Emitter } from "mitt";
|
||||||
|
|
||||||
|
export type Mitt = Emitter;
|
||||||
3
src/types/vue3/vueTransitionCallback/index.ts
Normal file
3
src/types/vue3/vueTransitionCallback/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export type VueTransitionCallback = {
|
||||||
|
(el?: SVGGElement | null): void;
|
||||||
|
}
|
||||||
3
src/types/vueRef/index.ts
Normal file
3
src/types/vueRef/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export interface Ref<A> {
|
||||||
|
value: A;
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<form id="login" @submit.prevent="submitForm">
|
<form id="login" @submit.prevent="submit">
|
||||||
|
|
||||||
|
|
||||||
<!-- login title -->
|
<!-- login title -->
|
||||||
<div id="loginTitle">
|
<div id="loginTitle">
|
||||||
|
|
@ -11,14 +10,14 @@
|
||||||
<div id="loginBox">
|
<div id="loginBox">
|
||||||
<!-- username -->
|
<!-- username -->
|
||||||
<div class="inputBox">
|
<div class="inputBox">
|
||||||
<input class="input" type="text" v-model="usr" />
|
<input class="input" type="text" v-model="email" />
|
||||||
<div class="inputModifier">Email</div>
|
<div class="inputModifier">Email</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<!-- password -->
|
<!-- password -->
|
||||||
<div class="inputBox">
|
<div class="inputBox">
|
||||||
<input class="input" type="password" v-model="pwd" />
|
<input class="input" type="password" v-model="password" />
|
||||||
<div class="inputModifier">Password</div>
|
<div class="inputModifier">Password</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -29,52 +28,61 @@
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!-- back to login button: position: fixed -->
|
<!-- back to login button: position: fixed -->
|
||||||
<!-- <button class="createAccountButton button" @click.prevent="switchView">Create account</button> -->
|
<button class="createAccountButton button" @click.prevent="switchView">Create account</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
|
||||||
import { defineComponent, ref } from "vue";
|
import { defineComponent, ref } from "vue";
|
||||||
import { setProfile } from '@/render/composables/useProfile';
|
import { useAuth } from "@/modules/auth";
|
||||||
import { invokeLogin } from "@/render/ipc";
|
import { useIpc } from "@/modules/ipc";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
|
||||||
|
interface LoginPayload {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
rememberMe: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: "Login",
|
name: "Login",
|
||||||
|
|
||||||
setup() {
|
setup() {
|
||||||
|
const { setToken } = useAuth();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const usr = ref("");
|
const email = ref("");
|
||||||
const pwd = ref("");
|
const password = ref("");
|
||||||
|
const rememberMe = ref(true);
|
||||||
|
|
||||||
// Submit login credentials to the backend.
|
const { invoke } = useIpc();
|
||||||
const submitForm = async () => {
|
|
||||||
|
|
||||||
|
const submit = async () => {
|
||||||
|
const payload = {
|
||||||
|
request: "login",
|
||||||
|
email: email.value,
|
||||||
|
password: password.value
|
||||||
|
};
|
||||||
try {
|
try {
|
||||||
|
const res = await invoke('auth-session', payload);
|
||||||
const profile = await invokeLogin({
|
setToken(res);
|
||||||
email: usr.value,
|
router.push({ name: "home" });
|
||||||
password: pwd.value
|
|
||||||
}) as Profile;
|
|
||||||
|
|
||||||
setProfile(profile)
|
|
||||||
|
|
||||||
|
|
||||||
} catch(e){
|
} catch(e){
|
||||||
console.log(e);
|
console.log('Error loging in.')
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
}
|
const switchView = () => {
|
||||||
|
router.push({ name: "register" });
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
usr,
|
submit,
|
||||||
pwd,
|
email,
|
||||||
submitForm
|
password,
|
||||||
}
|
rememberMe,
|
||||||
|
switchView
|
||||||
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
@ -102,7 +110,7 @@ export default defineComponent({
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding-bottom: 20px;
|
padding-bottom: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inputBox {
|
.inputBox {
|
||||||
|
|
@ -169,12 +177,4 @@ export default defineComponent({
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.invalid {
|
|
||||||
margin-bottom: 30px;
|
|
||||||
font-family: "SF Compact Display";
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #F53737;
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
202
src/views/messenger.vue
Normal file
202
src/views/messenger.vue
Normal file
|
|
@ -0,0 +1,202 @@
|
||||||
|
<template>
|
||||||
|
|
||||||
|
<TextAudioInput />
|
||||||
|
|
||||||
|
<!-- List of message bubbles. -->
|
||||||
|
<div id="messenger">
|
||||||
|
|
||||||
|
<!-- message box (view width) -->
|
||||||
|
<div
|
||||||
|
class="message"
|
||||||
|
:id="`${message.modifier}Message`"
|
||||||
|
v-for="message in messages"
|
||||||
|
>
|
||||||
|
|
||||||
|
<!-- 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>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
|
||||||
|
import { defineComponent, computed, reactive, onMounted, ref } from "vue";
|
||||||
|
|
||||||
|
import TextAudioInput from "@/components/taInput/index.vue";
|
||||||
|
import { Message } from "@/types/message/index";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: "Messenger",
|
||||||
|
|
||||||
|
components: {
|
||||||
|
TextAudioInput
|
||||||
|
},
|
||||||
|
|
||||||
|
setup(props) {
|
||||||
|
|
||||||
|
// List of messages in the view.
|
||||||
|
// Oldest message first.
|
||||||
|
const messages: Message[] = reactive([])
|
||||||
|
|
||||||
|
// Render a message.
|
||||||
|
const render = (message: Message) => {
|
||||||
|
messages.push(message)
|
||||||
|
console.log(`Pushing: ${message.modifier}: ${message.avatar}, ${message.content}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
|
||||||
|
// Listen for new messages to render.
|
||||||
|
window.ipcRenderer.on("render-message", (event, payload) => {
|
||||||
|
if (payload.message.content) render(payload.message);
|
||||||
|
});
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
messages,
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
|
#messenger {
|
||||||
|
position: fixed;
|
||||||
|
|
||||||
|
width: 100vw;
|
||||||
|
min-height: 100vh;
|
||||||
|
|
||||||
|
overflow-y: scroll;
|
||||||
|
|
||||||
|
background-color: #EBEBEB;
|
||||||
|
|
||||||
|
bottom: 0;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#messenger::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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>
|
||||||
165
src/views/register.vue
Normal file
165
src/views/register.vue
Normal file
|
|
@ -0,0 +1,165 @@
|
||||||
|
<template>
|
||||||
|
<div id="register">
|
||||||
|
<!-- login title -->
|
||||||
|
<div id="registerTitle">
|
||||||
|
<div>Register</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- username and password forms -->
|
||||||
|
<form id="registerBox" @submit.prevent="submit">
|
||||||
|
<!-- username -->
|
||||||
|
<input class="input" type="text" v-model="email" placeholder="email" required/>
|
||||||
|
|
||||||
|
<!-- password -->
|
||||||
|
<input class="input" type="password" v-model="password" placeholder="Password" required/>
|
||||||
|
|
||||||
|
<!-- re-enter passoword -->
|
||||||
|
<input class="input" type="password" v-model="password2" placeholder="Re-enter password" required/>
|
||||||
|
|
||||||
|
<!-- submit button; position: fixed -->
|
||||||
|
<button class="button" type="submit">Submit</button>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- back to login button: position: fixed -->
|
||||||
|
<button class="button2" @click.prevent="switchView">Back to login</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import {
|
||||||
|
defineComponent,
|
||||||
|
ref,
|
||||||
|
} from "vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
import { useIpc } from "@/modules/ipc";
|
||||||
|
import { useAuth } from "@/modules/auth";
|
||||||
|
|
||||||
|
interface RegisterPayload {
|
||||||
|
request: string;
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: "Register",
|
||||||
|
setup() {
|
||||||
|
const email = ref("");
|
||||||
|
const password = ref("");
|
||||||
|
const password2 = ref("");
|
||||||
|
|
||||||
|
const { invoke } = useIpc();
|
||||||
|
const { setToken } = useAuth();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const re = /^(([^<>()\[\]\.,;:\s@\"]+(\.[^<>()\[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i;
|
||||||
|
|
||||||
|
const passwordMatch = () => {
|
||||||
|
let match = false;
|
||||||
|
if (password.value == password2.value) {
|
||||||
|
match = true;
|
||||||
|
}
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
|
||||||
|
const validate = () => {
|
||||||
|
let valid = false;
|
||||||
|
if (re.test(email.value) && passwordMatch()) {
|
||||||
|
valid = true;
|
||||||
|
}
|
||||||
|
return valid;
|
||||||
|
}
|
||||||
|
|
||||||
|
const submit = async () => {
|
||||||
|
|
||||||
|
const payload: RegisterPayload = {
|
||||||
|
request: "register",
|
||||||
|
email: email.value,
|
||||||
|
password: password.value,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (validate()) {
|
||||||
|
try {
|
||||||
|
const res = await invoke('auth-session', payload);
|
||||||
|
setToken(res);
|
||||||
|
router.push({ name: "home" });
|
||||||
|
} catch(e) {
|
||||||
|
console.log('Failed to register.')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const switchView = () => {
|
||||||
|
router.push({ name: "login" });
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
password2,
|
||||||
|
switchView,
|
||||||
|
submit,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
#register {
|
||||||
|
width: 350px;
|
||||||
|
height: 500px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: #e6e6e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
#registerTitle {
|
||||||
|
min-width: 181px;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
#registerBox {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
background-color: #B9B9B9;
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
padding: 16px;
|
||||||
|
text-decoration: none;
|
||||||
|
margin: 4px 2px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
position: fixed;
|
||||||
|
margin-top: 165px;
|
||||||
|
background-color: #58C4FD;
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
padding: 10px 20px;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button2 {
|
||||||
|
position: fixed;
|
||||||
|
margin-top: 225px;
|
||||||
|
border: none;
|
||||||
|
background-color: #e6e6e6;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #58C4FD;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -49,7 +49,9 @@
|
||||||
import { defineComponent } from "vue";
|
import { defineComponent } from "vue";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
|
|
||||||
name: "Splash",
|
name: "Splash",
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
161
src/window.ts
161
src/window.ts
|
|
@ -1,161 +0,0 @@
|
||||||
"use strict";
|
|
||||||
|
|
||||||
import { BrowserWindow, ipcMain, app } from "electron";
|
|
||||||
import { createProtocol } from "vue-cli-plugin-electron-builder/lib";
|
|
||||||
import { backgroundMitt } from './composables/useEmitter';
|
|
||||||
import { saveToJson } from "./composables/useSaveToJSON";
|
|
||||||
import * as path from "path";
|
|
||||||
import fs from 'fs';
|
|
||||||
import { config } from "@/config";
|
|
||||||
const { autoUpdater } = require('electron-updater');
|
|
||||||
|
|
||||||
let win: BrowserWindow | null;
|
|
||||||
let winState: WindowState;
|
|
||||||
|
|
||||||
const loadWinState = (fileName: string): WindowState => {
|
|
||||||
let state: WindowState;
|
|
||||||
|
|
||||||
try {
|
|
||||||
state = JSON.parse(fs.readFileSync(config.configPath + fileName).toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
catch (error) {
|
|
||||||
state = {
|
|
||||||
width: 600,
|
|
||||||
height: 500,
|
|
||||||
x: null,
|
|
||||||
y: null,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return state
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Called when a NavBar button is pressed.
|
|
||||||
const onNavBar = (_event: any, action: string): void => {
|
|
||||||
if (win) {
|
|
||||||
if (action === "close") {
|
|
||||||
win.close()
|
|
||||||
} else {
|
|
||||||
win.minimize()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Util function to render message on ipc-renderer event.
|
|
||||||
const postToWindow = <T>(event: IpcRendererEvent<T>): void => {
|
|
||||||
if (win) {
|
|
||||||
win.webContents.send(event.channel, event.payload);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write a json with position and size of window.
|
|
||||||
const saveWindowState = () => {
|
|
||||||
if (win) {
|
|
||||||
const bounds = win.getBounds();
|
|
||||||
const position = win.getPosition();
|
|
||||||
|
|
||||||
if (winState) {
|
|
||||||
|
|
||||||
winState.width = bounds.width;
|
|
||||||
winState.height = bounds.height;
|
|
||||||
winState.x = position[0];
|
|
||||||
winState.y = position[1]
|
|
||||||
|
|
||||||
saveToJson("window.json", winState)
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Do this on window mount.
|
|
||||||
const onWindowMount = (): void => {
|
|
||||||
|
|
||||||
// Must tell initApp that window exists.
|
|
||||||
backgroundMitt.emit('window-active', true);
|
|
||||||
|
|
||||||
// Handle win nav-bar event.
|
|
||||||
ipcMain.removeAllListeners("nav-bar") // avoid setting duplicate handlers
|
|
||||||
ipcMain.on("nav-bar", onNavBar);
|
|
||||||
|
|
||||||
// Gateway for messages to the frontend.
|
|
||||||
backgroundMitt.removeAllListeners("ipc-renderer")
|
|
||||||
backgroundMitt.on("ipc-renderer", postToWindow);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Do this on window dismount (close).
|
|
||||||
const onWindowDismount = (): void => {
|
|
||||||
win = null;
|
|
||||||
backgroundMitt.emit('window-active', false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// function used by run.ts to create the main window.
|
|
||||||
export default async function createWindow(): Promise<void> {
|
|
||||||
return new Promise((resolve, _reject) => {
|
|
||||||
|
|
||||||
// avoid creating duplicate windows.
|
|
||||||
if (win) resolve();
|
|
||||||
|
|
||||||
// Load the saved window state.
|
|
||||||
winState = loadWinState("window.json")
|
|
||||||
|
|
||||||
// Define the browser window.
|
|
||||||
win = new BrowserWindow({
|
|
||||||
width: winState.width,
|
|
||||||
height: winState.height,
|
|
||||||
x: winState.x as number,
|
|
||||||
y: winState.y as number,
|
|
||||||
resizable: true,
|
|
||||||
backgroundColor: '#EBEBEB',
|
|
||||||
frame: false,
|
|
||||||
minWidth: 350,
|
|
||||||
minHeight: 500,
|
|
||||||
webPreferences: {
|
|
||||||
nodeIntegration: (process.env.ELECTRON_NODE_INTEGRATION as unknown) as boolean, preload: path.join(__dirname, "preload.js")
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Load the URL.
|
|
||||||
if (process.env.WEBPACK_DEV_SERVER_URL) {
|
|
||||||
win.loadURL(process.env.WEBPACK_DEV_SERVER_URL as string); // dev
|
|
||||||
}
|
|
||||||
|
|
||||||
else {
|
|
||||||
createProtocol("app");
|
|
||||||
win.loadURL("app://./index.html"); // prod
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle window close.
|
|
||||||
win.on("closed", onWindowDismount);
|
|
||||||
|
|
||||||
// Save window state on resize and move.
|
|
||||||
win.on("moved", saveWindowState);
|
|
||||||
win.on("resize", saveWindowState);
|
|
||||||
|
|
||||||
win.once('ready-to-show', () => {
|
|
||||||
autoUpdater.checkForUpdatesAndNotify();
|
|
||||||
if (win) win.show()
|
|
||||||
})
|
|
||||||
|
|
||||||
// For showing/hiding the splash screen.
|
|
||||||
win.webContents.on('did-finish-load', () => {
|
|
||||||
if (win) win.webContents.send('window-ready', {
|
|
||||||
message: true
|
|
||||||
});
|
|
||||||
|
|
||||||
onWindowMount();
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
autoUpdater.on('update-available', () => {
|
|
||||||
if (win) win.webContents.send('update_available');
|
|
||||||
});
|
|
||||||
|
|
||||||
autoUpdater.on('update-downloaded', () => {
|
|
||||||
if (win) win.webContents.send('update_downloaded');
|
|
||||||
});
|
|
||||||
155
tests/audio.js
155
tests/audio.js
|
|
@ -10,11 +10,6 @@ const encoding = 'LINEAR16';
|
||||||
const sampleRateHertz = 16000;
|
const sampleRateHertz = 16000;
|
||||||
const languageCode = 'en-US';
|
const languageCode = 'en-US';
|
||||||
|
|
||||||
const audioContainer = {
|
|
||||||
input: '',
|
|
||||||
buffers: []
|
|
||||||
}
|
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
encoding: encoding,
|
encoding: encoding,
|
||||||
sampleRateHertz: sampleRateHertz,
|
sampleRateHertz: sampleRateHertz,
|
||||||
|
|
@ -25,6 +20,8 @@ const config = {
|
||||||
* Note that transcription is limited to 60 seconds audio.
|
* Note that transcription is limited to 60 seconds audio.
|
||||||
* Use a GCS file for audio longer than 1 minute.
|
* Use a GCS file for audio longer than 1 minute.
|
||||||
*/
|
*/
|
||||||
|
let audio;
|
||||||
|
|
||||||
async function transcribeSpeech (audio) {
|
async function transcribeSpeech (audio) {
|
||||||
const request = {
|
const request = {
|
||||||
config: config,
|
config: config,
|
||||||
|
|
@ -44,8 +41,7 @@ async function transcribeSpeech (audio) {
|
||||||
console.log(`Transcription: ${transcription}`);
|
console.log(`Transcription: ${transcription}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
let record = true;
|
let audioInput = [];
|
||||||
|
|
||||||
// Create an instance of AudioIO with inOptions (defaults are as below), which will return a ReadableStream
|
// Create an instance of AudioIO with inOptions (defaults are as below), which will return a ReadableStream
|
||||||
const ia = new portAudio.AudioIO({
|
const ia = new portAudio.AudioIO({
|
||||||
inOptions: {
|
inOptions: {
|
||||||
|
|
@ -54,129 +50,46 @@ const ia = new portAudio.AudioIO({
|
||||||
sampleRate: 16000,
|
sampleRate: 16000,
|
||||||
deviceId: -1,
|
deviceId: -1,
|
||||||
closeOnError: false,
|
closeOnError: false,
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
ia.setEncoding('base64');
|
ia.setEncoding('base64');
|
||||||
ia.start();
|
ia.start();
|
||||||
|
ia.on('error', (e) => {
|
||||||
|
console.log('error recording audio', + e);
|
||||||
|
});
|
||||||
ia.on('data', (chunk) => {
|
ia.on('data', (chunk) => {
|
||||||
if (record) {
|
audioInput.push(chunk);
|
||||||
console.log('recording data');
|
console.log('Got %d characters of string data:', chunk.length);
|
||||||
audioContainer.input += chunk;
|
|
||||||
} else {
|
|
||||||
if (audioContainer.input.length) audioContainer.input = "";
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const ao = new portAudio.AudioIO({
|
async function processAudio() {
|
||||||
outOptions: {
|
audio = '';
|
||||||
sampleFormat: 16,
|
audioInput.forEach(s => {
|
||||||
channelCount: 1,
|
audio += s;
|
||||||
sampleRate: 16000,
|
})
|
||||||
deviceId: -1,
|
const buffer = Buffer.from(audio, 'base64');
|
||||||
closeOnError: false,
|
audioInput = [];
|
||||||
}
|
await transcribeSpeech({
|
||||||
|
content: buffer
|
||||||
});
|
});
|
||||||
ao.start();
|
|
||||||
|
|
||||||
let counter = 0;
|
|
||||||
const tests = [];
|
|
||||||
|
|
||||||
function testCallback() {
|
|
||||||
play(tests[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
function splitArrayIntoChunksOfLen(buf, len) {
|
|
||||||
const chunks = [];
|
|
||||||
let i = 0;
|
|
||||||
let L = len;
|
|
||||||
console.log(buf.byteLength)
|
|
||||||
while(i < buf.byteLength) {
|
|
||||||
chunks.push(buf.slice(i, L));
|
|
||||||
i = L;
|
|
||||||
L += len;
|
|
||||||
}
|
|
||||||
|
|
||||||
chunks.forEach(c => console.log(c))
|
|
||||||
return chunks;
|
|
||||||
}
|
|
||||||
|
|
||||||
function play(input) {
|
|
||||||
let i = 0;
|
|
||||||
const buf = Buffer.from(input, 'base64');
|
|
||||||
// format buffers into half their size to account for writable highwaterMark.
|
|
||||||
const audio = splitArrayIntoChunksOfLen(buf, 8192);
|
|
||||||
// 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 ok = true;
|
|
||||||
do {
|
|
||||||
if (i === audio.length - 1) {
|
|
||||||
// write last chunk.
|
|
||||||
ao.write(audio[i], null, callback);
|
|
||||||
} else {
|
|
||||||
// check for backpreassure.
|
|
||||||
ok = ao.write(audio[i], 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// utility function used by play func
|
|
||||||
function bufSplit(input){
|
|
||||||
const result = [];
|
|
||||||
input.forEach((b) => {
|
|
||||||
// split buffer into two.
|
|
||||||
result.push(b.slice(0, b.length / 2), b.slice(b.length / 2, b.length));
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function test() {
|
|
||||||
transcribeSpeech({
|
|
||||||
content: Buffer.from(audioContainer.input, 'base64')
|
|
||||||
});
|
|
||||||
tests.push(audioContainer.input)
|
|
||||||
counter++;
|
|
||||||
console.log('audio string length:', audioContainer.input.length)
|
|
||||||
console.log('buffers written: ', audioContainer.buffers.length)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
record = false;
|
ia.pause();
|
||||||
test()
|
await processAudio();
|
||||||
}, 4000);
|
// ia.resume();
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
ia.resume();
|
||||||
|
}, 4000)
|
||||||
|
|
||||||
// setTimeout(() => {
|
|
||||||
// record = true;
|
|
||||||
// }, 6000)
|
|
||||||
//
|
|
||||||
// setTimeout(async () => {
|
|
||||||
// record = false;
|
|
||||||
// test();
|
|
||||||
// }, 9000);
|
|
||||||
//
|
|
||||||
// setTimeout(() => {
|
|
||||||
// record = true;
|
|
||||||
// }, 11000)
|
|
||||||
//
|
|
||||||
// setTimeout(async () => {
|
|
||||||
// record = false;
|
|
||||||
// test();
|
|
||||||
// }, 14000);
|
|
||||||
//
|
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
ia.quit();
|
ia.pause();
|
||||||
testCallback()
|
await processAudio();
|
||||||
return;
|
// ia.resume();
|
||||||
}, 6000);
|
}, 8000);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
ia.pause();
|
||||||
|
}, 9000)
|
||||||
|
|
|
||||||
|
|
@ -3,30 +3,14 @@ const WebSocket = require("ws");
|
||||||
const wss = new WebSocket.Server({
|
const wss = new WebSocket.Server({
|
||||||
port: 8760
|
port: 8760
|
||||||
});
|
});
|
||||||
let auth = false;
|
|
||||||
|
|
||||||
wss.on("connection", function connection(ws, req) {
|
wss.on("connection", function connection(ws, req) {
|
||||||
|
|
||||||
ws.on("message", function incoming(message) {
|
ws.on("message", function incoming(message) {
|
||||||
|
|
||||||
console.log(message)
|
console.log(message)
|
||||||
if (message === 'no-token' || message === 'undefined') {
|
if (message === 'no-token' || message === 'undefined') {
|
||||||
ws.send('locked');
|
console.log('no token to authenticate');
|
||||||
} else if (message instanceof String || typeof message === "string") {
|
|
||||||
// audio message
|
|
||||||
console.log('caught the token', message)
|
|
||||||
if (!auth) {
|
|
||||||
ws.send('success');
|
ws.send('success');
|
||||||
auth = true;
|
|
||||||
} else {
|
|
||||||
const parsed = JSON.parse(message);
|
|
||||||
console.log('got a message', message);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
const parsed = JSON.parse(message);
|
|
||||||
console.log('parsed', parsed);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue