Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
95e3dab1c2 | ||
|
|
2c510765c0 | ||
|
|
56196bcbcf | ||
|
|
c8411d573a | ||
|
|
a89e856661 | ||
|
|
ab487e4f5e | ||
|
|
a22b3c68e5 | ||
|
|
a6351af83c | ||
|
|
798b848c9b | ||
|
|
d963f7d8a8 | ||
|
|
0a3c9f71d8 | ||
|
|
d851db2fcc | ||
|
|
9e8641b4de | ||
|
|
3b5da6124f | ||
|
|
628210dab5 | ||
|
|
25b03d501c | ||
|
|
0cfa2340be | ||
|
|
0079c72ea0 | ||
|
|
ac8f338d75 | ||
|
|
7dbf4e6aa6 | ||
|
|
716730fdd2 | ||
|
|
87dce82c53 | ||
|
|
ddb2f4df0f | ||
|
|
17e4b06948 | ||
|
|
69dc3177ee | ||
|
|
6db693f9d3 | ||
|
|
730cb4b31a | ||
|
|
7c2f3cad8c | ||
|
|
aab9ae136d | ||
|
|
451e0380ac | ||
|
|
ce0e7af363 | ||
|
|
3bcf1b594d | ||
|
|
ff5db54ac1 | ||
|
|
17bcda5906 | ||
|
|
36491c5894 | ||
|
|
0f18d8fca2 | ||
|
|
0ac4a5ba63 | ||
|
|
72b3368b48 | ||
|
|
72fd4aa6e0 | ||
|
|
a166753c5b | ||
|
|
6da8bf4b7a | ||
|
|
ddffe1a40a | ||
|
|
e638062021 | ||
|
|
594011f8ce | ||
|
|
276378b77f | ||
|
|
0c3be6442c | ||
|
|
7e8ebf72db | ||
|
|
288cfe2100 | ||
|
|
216290dd52 | ||
|
|
b058789729 | ||
|
|
babb85ad7c | ||
|
|
6dc4d56818 | ||
|
|
7bd806cc92 | ||
|
|
ab8765697a | ||
|
|
9a849474bd | ||
|
|
0eb6c654d9 | ||
|
|
5f3e14f557 | ||
|
|
2c89119092 | ||
|
|
687762bca4 | ||
|
|
a01507b2bd | ||
|
|
f7e70f02fa | ||
|
|
5fe6e4f047 | ||
|
|
8597d4899a | ||
|
|
b566923896 | ||
|
|
d9582a8f3a | ||
|
|
bb437e1b5f | ||
|
|
97a6514966 | ||
|
|
c1285893a4 | ||
|
|
9283c05115 | ||
|
|
a17c552b66 | ||
|
|
809ec5e05a | ||
|
|
c3be798dda | ||
|
|
ee203d93ba | ||
|
|
662fc591c3 | ||
|
|
62a4311828 | ||
|
|
f66c9ccfc2 | ||
|
|
52b4e5ad2b | ||
|
|
df01258dfc | ||
|
|
d739349004 | ||
|
|
4b6d42612a | ||
|
|
db5a408c42 | ||
|
|
e4aa477f5f | ||
|
|
0005df2918 | ||
|
|
f322bf85da | ||
|
|
a1479c165b | ||
|
|
fa34bf989f | ||
|
|
0f987bc481 | ||
|
|
1c452d406b | ||
|
|
7d97756d60 | ||
|
|
f89c9944e7 | ||
|
|
81662942a2 | ||
|
|
fcac59a7a0 | ||
|
|
a63d5e6004 | ||
|
|
85c3799fca | ||
|
|
bb9769e917 | ||
|
|
9bf7496899 | ||
|
|
7dcfd9252a | ||
|
|
4b3023fd4c | ||
|
|
7bb0bda756 |
84 changed files with 2842 additions and 3142 deletions
|
|
@ -1,3 +0,0 @@
|
|||
> 1%
|
||||
last 2 versions
|
||||
not dead
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
|
|
@ -9,6 +9,7 @@ crash.log
|
|||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
.env
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
|
|
@ -27,3 +28,10 @@ pnpm-debug.log*
|
|||
|
||||
#Electron-builder output
|
||||
/dist_electron
|
||||
|
||||
#Electron-builder config
|
||||
electron-builder.yml
|
||||
|
||||
#Window and session states
|
||||
session.json
|
||||
window.json
|
||||
|
|
|
|||
63
.gitlab-ci.yml
Normal file
63
.gitlab-ci.yml
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
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
|
||||
```
|
||||
yarn dev
|
||||
yarn electron:serve
|
||||
```
|
||||
|
||||
### Compiles and minifies for production
|
||||
|
|
@ -15,7 +15,7 @@ yarn dev
|
|||
yarn electron:build
|
||||
```
|
||||
|
||||
### Run your unit tests
|
||||
<!-- ### Run your unit tests
|
||||
```
|
||||
yarn test:unit
|
||||
```
|
||||
|
|
@ -23,7 +23,7 @@ yarn test:unit
|
|||
### Lints and fixes files
|
||||
```
|
||||
yarn lint
|
||||
```
|
||||
``` -->
|
||||
|
||||
### Customize configuration
|
||||
See [Configuration Reference](https://cli.vuejs.org/config/).
|
||||
|
|
|
|||
|
|
@ -1,79 +0,0 @@
|
|||
# Do not edit. File was generated by node-gyp's "configure" step
|
||||
{
|
||||
"target_defaults": {
|
||||
"cflags": [],
|
||||
"default_configuration": "Release",
|
||||
"defines": [],
|
||||
"include_dirs": [],
|
||||
"libraries": []
|
||||
},
|
||||
"variables": {
|
||||
"asan": 0,
|
||||
"build_v8_with_gn": "false",
|
||||
"coverage": "false",
|
||||
"dcheck_always_on": 0,
|
||||
"debug_nghttp2": "false",
|
||||
"debug_node": "false",
|
||||
"enable_lto": "false",
|
||||
"enable_pgo_generate": "false",
|
||||
"enable_pgo_use": "false",
|
||||
"error_on_warn": "false",
|
||||
"force_dynamic_crt": 0,
|
||||
"host_arch": "x64",
|
||||
"icu_data_in": "../../deps/icu-tmp/icudt67l.dat",
|
||||
"icu_endianness": "l",
|
||||
"icu_gyp_path": "tools/icu/icu-generic.gyp",
|
||||
"icu_path": "deps/icu-small",
|
||||
"icu_small": "false",
|
||||
"icu_ver_major": "67",
|
||||
"is_debug": 0,
|
||||
"llvm_version": "0.0",
|
||||
"napi_build_version": "6",
|
||||
"node_byteorder": "little",
|
||||
"node_debug_lib": "false",
|
||||
"node_enable_d8": "false",
|
||||
"node_install_npm": "true",
|
||||
"node_module_version": 83,
|
||||
"node_no_browser_globals": "false",
|
||||
"node_prefix": "/",
|
||||
"node_release_urlbase": "https://nodejs.org/download/release/",
|
||||
"node_shared": "false",
|
||||
"node_shared_brotli": "false",
|
||||
"node_shared_cares": "false",
|
||||
"node_shared_http_parser": "false",
|
||||
"node_shared_libuv": "false",
|
||||
"node_shared_nghttp2": "false",
|
||||
"node_shared_openssl": "false",
|
||||
"node_shared_zlib": "false",
|
||||
"node_tag": "",
|
||||
"node_target_type": "executable",
|
||||
"node_use_bundled_v8": "true",
|
||||
"node_use_dtrace": "true",
|
||||
"node_use_etw": "false",
|
||||
"node_use_node_code_cache": "true",
|
||||
"node_use_node_snapshot": "true",
|
||||
"node_use_openssl": "true",
|
||||
"node_use_v8_platform": "true",
|
||||
"node_with_ltcg": "false",
|
||||
"node_without_node_options": "false",
|
||||
"openssl_fips": "",
|
||||
"openssl_is_fips": "false",
|
||||
"shlib_suffix": "83.dylib",
|
||||
"target_arch": "x64",
|
||||
"v8_enable_31bit_smis_on_64bit_arch": 0,
|
||||
"v8_enable_gdbjit": 0,
|
||||
"v8_enable_i18n_support": 1,
|
||||
"v8_enable_inspector": 1,
|
||||
"v8_enable_pointer_compression": 0,
|
||||
"v8_no_strict_aliasing": 1,
|
||||
"v8_optimized_debug": 1,
|
||||
"v8_promise_internal_field_count": 1,
|
||||
"v8_random_seed": 0,
|
||||
"v8_trace_maps": 0,
|
||||
"v8_use_siphash": 1,
|
||||
"want_separate_host_toolset": 0,
|
||||
"xcode_version": "11.0",
|
||||
"nodedir": "/Users/Enrique/Library/Caches/node-gyp/14.4.0",
|
||||
"standalone_static_library": 1
|
||||
}
|
||||
}
|
||||
12
gcloud.json
12
gcloud.json
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"type": "service_account",
|
||||
"project_id": "crimata",
|
||||
"private_key_id": "3a324fa3fcca4cea589e4b5007f4142db4b3a84d",
|
||||
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCxFJOjhIkYlyhn\nHjtpFC5EOwzNFZem6k6SS7zcgWbNLe9mcBsiTKCjpxItzkIo0pnT32yqw+AKsqpv\n6+LVyzzjfB7vtEUflvg6KgmYMS4ixOTCEbN3e2qj1t2RnITXZR2VGSGdoKKyqZm0\ntWrmexQvSN20cByL1vfxD3E69oYFGhVraa0zmm94WXHAzofT6DkvGdrbK1s1G9jI\nG3mdQ1jN71tghyloeHL+R/1iKQoZFYTkVe6yWvccSNAZelt/5sciCKRTCArXj89X\nLwU6jGOKIsyqx3BeUyWaSS59xpnGX3meSf5Q9U+j45ePUiqO6TTleF+kWiBwjUpp\nfeYcwEIDAgMBAAECggEAElkN9R7p75zV6F1XDYu0QYiWyncmx/o2Gu1zC6vywWa1\ni/korpSe/mX0ub9J1p3/p1balRUHlUQu6brYvYs56140fGTC1sOXQ7uQU+8glySs\niTk5TbOBeKluOsSrdP/6oTTB6Wm4AegVz4YOpgPxsvaLVkNZidnDxfvyIQhjGYsO\nsvRaw48eEmGOa1uvDhgvPOxiDp5YEdxqSue/7ylzjBdKecDyLgTSDDf/p+FwIx5B\n2d/0uRgQBaAk6D/DNOvN8GrYyakFmlDjE72nrBGAJq50UyV6THOSDk+8cK+2QiS1\nEDCFufj+7Dk1EBfBFUFY8ZBB/pphpEWyLoFl7NzGCQKBgQDaj/KOOcj6BkkTj1WM\nRW3KhOE3nhsCQEgQGCflpycEAZj6J7ktBG2/4teDt9sr+Epx8ymnQCZa9pmysdrD\nU0aWku1qUx0IUKTtZbAEbB+AnrWYDYkIWU8kRUUvNHNTBg43VyuohV9wprPih138\nHWMeNac82/XICz2A3rv+Xkx8KwKBgQDPaaEEGWVs3nIhf/p3M3/xWNKt8fhAERpG\n0YpTjgHW3FQFYjiCTTIcuLO3qsDGmfIAB9tL37s2Y3e2wKBGUj/o69Fr7sWVPdsX\nJtUc0+KiG2Bs0hsyZPdk9UQiQX8y2eq1NZW7vEoxWso1bM64WURB5N8ocWePNJJx\nL2Oe75rtiQKBgQDLfQQOiRxmNF3rOSMkAywyRr9NQgXRdbniSiszNQotP7OHDF7q\n29m2suOGfjIv4O6m4wdf8WkEfd4hsleETc9Ft6wVtyYrrLGxWWCk7WnzHVDjLY7s\n2AHIOjostf+9R8EKoz1BnFN8laibev71EQNMiBWZow1VX6m2hymurWs2mwKBgQCb\nuY3n2v14qOb92e1+U89KsEq1yMd/qpeU9jwqEaO14wS+aglNY5ItWEuuqWhFdE3q\n0ftHUzpnUnUOZD+xrI1JXsyEgegc7i0xi7lUBI3S8kUKTxGWW5IXXcKDCbPrxQtg\ndFPweSUnOyg4xnHKnVMPOjyGS+bZ8TnF+zOLoBAtKQKBgF6hAOIwF1DheXT4p8xS\nlSGhl8dBxjsJDVwbnFWQceGzGnq1e3ncy/UItt42UCDVRWTH4r7pQ9eKyd/3foUD\n6KDIcCrYo2KJh6ZNFJ67XF30U+TuEg5U/9jTbWqDOwYVXcMcnzQE4ZiGBxVHL6xr\nNc8FRW5+7xh4zNfzhGUGt5kt\n-----END PRIVATE KEY-----\n",
|
||||
"client_email": "crimata-service-account@crimata.iam.gserviceaccount.com",
|
||||
"client_id": "102914612880037864172",
|
||||
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
||||
"token_uri": "https://oauth2.googleapis.com/token",
|
||||
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
||||
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/crimata-service-account%40crimata.iam.gserviceaccount.com"
|
||||
}
|
||||
45
login.svg
45
login.svg
|
|
@ -1,45 +0,0 @@
|
|||
<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>
|
||||
|
Before Width: | Height: | Size: 1.9 KiB |
54
package.json
54
package.json
|
|
@ -1,24 +1,18 @@
|
|||
{
|
||||
"name": "Crimata",
|
||||
"productName": "crimata-messenger",
|
||||
"description": "Cross-platform messenger application built with electron, vue3, and TS.",
|
||||
"version": "0.1.0",
|
||||
"version": "0.9.9",
|
||||
"private": true,
|
||||
"description": "Cross-platform messenger application built with electron, vue3, and TS.",
|
||||
"author": {
|
||||
"name": "Enrique Hernandez"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "yarn electron:serve",
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"test:unit": "vue-cli-service test:unit",
|
||||
"lint": "vue-cli-service lint",
|
||||
"electron:build": "vue-cli-service electron:build",
|
||||
"electron:serve": "vue-cli-service electron:serve",
|
||||
"build": "vue-cli-service electron:build",
|
||||
"dev": "vue-cli-service electron:serve",
|
||||
"postinstall": "electron-builder install-app-deps",
|
||||
"postuninstal": "electron-builder install-app-deps"
|
||||
"postuninstall": "electron-builder install-app-deps"
|
||||
},
|
||||
"main": "background.js",
|
||||
"main": "init.js",
|
||||
"dependencies": {
|
||||
"@google-cloud/speech": "^4.2.0",
|
||||
"@types/animejs": "^3.1.2",
|
||||
|
|
@ -28,10 +22,15 @@
|
|||
"@types/uuid": "^8.3.0",
|
||||
"@types/ws": "^7.2.7",
|
||||
"animejs": "^3.2.0",
|
||||
"axios": "^0.21.1",
|
||||
"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",
|
||||
"naudiodon": "^2.3.2",
|
||||
"node-record-lpcm16": "^1.0.1",
|
||||
"update-electron-app": "^2.0.1",
|
||||
"uuid": "^8.3.2",
|
||||
"vue": "^3.0.0-0",
|
||||
"vue-router": "^4.0.0-0",
|
||||
|
|
@ -39,6 +38,8 @@
|
|||
"ws": "^7.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/axios": "^0.14.0",
|
||||
"@types/electron-devtools-installer": "^2.2.0",
|
||||
"@types/jest": "^24.0.19",
|
||||
"@typescript-eslint/eslint-plugin": "^2.33.0",
|
||||
"@typescript-eslint/parser": "^2.33.0",
|
||||
|
|
@ -55,17 +56,36 @@
|
|||
"@wasm-tool/wasm-pack-plugin": "^1.3.1",
|
||||
"electron": "^9.0.0",
|
||||
"electron-devtools-installer": "^3.1.0",
|
||||
"electron-log": "^4.3.4",
|
||||
"eslint": "^6.7.2",
|
||||
"eslint-plugin-vue": "^7.0.0-0",
|
||||
"lint-staged": "^9.5.0",
|
||||
"node-sass": "^4.12.0",
|
||||
"optimize-wasm-webpack-plugin": "^1.0.12",
|
||||
"sass-loader": "^8.0.2",
|
||||
"spectron": "^11.0.0",
|
||||
"spectron": "11.0.0",
|
||||
"typescript": "~3.9.3",
|
||||
"vue-cli-plugin-electron-builder": "~2.0.0-rc.4",
|
||||
"vue-cli-plugin-electron-builder": "~2.0.0-rc.6",
|
||||
"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": {
|
||||
"pre-commit": "lint-staged"
|
||||
},
|
||||
|
|
@ -74,5 +94,11 @@
|
|||
"vue-cli-service lint",
|
||||
"git add"
|
||||
]
|
||||
},
|
||||
"productName": "crimata-messenger",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://gitlab.com/crimata/electron-app.git",
|
||||
"release": "latest"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,11 +11,7 @@
|
|||
<noscript>
|
||||
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<!-- <h1>Hello World!</h1>
|
||||
We are using Node.js <span id="node-version"></span>, Chromium
|
||||
<span id="chrome-version"></span>, and Electron
|
||||
<span id="electron-version"></span>. -->
|
||||
<div id="app"></div>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
144
src/App.vue
144
src/App.vue
|
|
@ -1,144 +0,0 @@
|
|||
<template>
|
||||
<div id="app" v-if="ready">
|
||||
|
||||
<button class="titlebar" />
|
||||
|
||||
<!-- Minimize and exit buttons. -->
|
||||
<span class="menu">
|
||||
<button
|
||||
class="menuButton exitButton"
|
||||
@click.prevent="callNavbar('close')"
|
||||
/>
|
||||
<button
|
||||
class="menuButton minimizeButton"
|
||||
@click.prevent="callNavbar('min')"
|
||||
/>
|
||||
</span>
|
||||
|
||||
<!-- Windows -->
|
||||
<router-view/>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Show spash screen if not ready -->
|
||||
<Splash v-else />
|
||||
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, onMounted, onUnmounted, ref } from 'vue';
|
||||
import { useIpc } from "@/modules/ipc";
|
||||
import Splash from "@/components/splash.vue"
|
||||
|
||||
export default defineComponent({
|
||||
components: { Splash },
|
||||
|
||||
setup() {
|
||||
|
||||
|
||||
// Show spash screen til true.
|
||||
const ready = ref(false);
|
||||
|
||||
const { invoke } = useIpc();
|
||||
|
||||
// Post navbar action to backend.
|
||||
const callNavbar = (action: string) => {
|
||||
invoke('nav-bar', action);
|
||||
}
|
||||
|
||||
const onWindowReady = (_event: any, _payload: any) => {
|
||||
ready.value = true;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.ipcRenderer.on("window-ready", onWindowReady);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.ipcRenderer.removeAllListeners("window-ready")
|
||||
})
|
||||
|
||||
return {
|
||||
callNavbar,
|
||||
ready
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
// Background color set in window.ts
|
||||
}
|
||||
|
||||
#app {
|
||||
font-family: "SF Pro Text";
|
||||
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
.titlebar {
|
||||
position: fixed;
|
||||
|
||||
width: 100vw;
|
||||
height: 54px;
|
||||
|
||||
opacity: 0.75;
|
||||
|
||||
background-color: #EBEBEB;
|
||||
|
||||
-webkit-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>
|
||||
95
src/account.ts
Normal file
95
src/account.ts
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
|
||||
import { postAuth, postLogin, postLogout } from "@/api/account";
|
||||
import { endSession, launchSession } from "@/session";
|
||||
import { getToken, setToken, setProfile, getProfile, clearStore } from "./store";
|
||||
import { parseAuthRes } from "./auth";
|
||||
import { ipcEmit } from "@/composables/useEmitter";
|
||||
|
||||
export const accountAuth = async (): Promise<Error | AuthState> => {
|
||||
|
||||
/* attempt to get a login token from the store */
|
||||
const token = getToken();
|
||||
|
||||
/* try to login with it, returns platform secret and new token on success */
|
||||
if (token) {
|
||||
try {
|
||||
|
||||
const res = await postAuth(token);
|
||||
|
||||
const parsed = parseAuthRes(res);
|
||||
|
||||
setToken(parsed.token)
|
||||
setProfile(parsed.profile);
|
||||
|
||||
return {
|
||||
profile: parsed.profile,
|
||||
token: parsed.token
|
||||
};
|
||||
|
||||
} catch(e) {
|
||||
console.log('[ACCOUNT]', e);
|
||||
clearStore();
|
||||
throw(new Error('Failed to authenticate.'));
|
||||
|
||||
}
|
||||
} else {
|
||||
throw(new Error('Unable to authenticate.'));
|
||||
}
|
||||
};
|
||||
|
||||
export const accountLogin: IpcHandlerCallback<AccountCredentials, Profile> = async (payload) => {
|
||||
const account = payload as AccountCredentials;
|
||||
try {
|
||||
|
||||
// attempt login with email password
|
||||
const res = await postLogin(account.email, account.password);
|
||||
const parsed = parseAuthRes(res);
|
||||
|
||||
// save jwt token and profile
|
||||
setToken(parsed.token);
|
||||
setProfile(parsed.profile);
|
||||
|
||||
// launch session
|
||||
launchSession(parsed.token);
|
||||
|
||||
// return profile to renderer
|
||||
return parsed.profile;
|
||||
|
||||
} catch(e) {
|
||||
clearStore();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const accountLogout = async (): Promise<Error | void> => {
|
||||
|
||||
try {
|
||||
// post logout to backend
|
||||
await postLogout();
|
||||
|
||||
// remove key and crimataId
|
||||
clearStore();
|
||||
|
||||
// kill crimata platform session
|
||||
endSession();
|
||||
|
||||
return;
|
||||
|
||||
} catch(e) {
|
||||
console.log('[ACCOUNT]', e);
|
||||
return (new Error('Failed to logout. Please try again.'));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const updateAppState = (): void => {
|
||||
|
||||
const profile = getProfile();
|
||||
|
||||
ipcEmit("set-profile", profile);
|
||||
|
||||
// ipcEmit('messages') etc
|
||||
|
||||
}
|
||||
|
||||
31
src/api/account.ts
Normal file
31
src/api/account.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
|
||||
import useHttp from "@/composables/useHttp";
|
||||
import axios from "axios";
|
||||
import {config} from "@/config";
|
||||
|
||||
const { post } = useHttp();
|
||||
|
||||
export const postAuth = async (token: string) => (
|
||||
await axios({
|
||||
url: config.BUSINESS_URL + config.BUSINESS_PREFIX + '/account/authenticate',
|
||||
headers: {
|
||||
Cookie: `crimataCookie=${token}`
|
||||
},
|
||||
method: 'POST',
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
export const postLogin = async (email: string, password: string) => (
|
||||
await post('/account/login', { email, password })
|
||||
);
|
||||
|
||||
|
||||
export const postLogout =
|
||||
async (): Promise<null | Error> => (await post('/account/logout'));
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="13.795" height="12.759" viewBox="0 0 13.795 12.759">
|
||||
<g id="Group_544" data-name="Group 544" transform="translate(-4127 -507)">
|
||||
<g id="Group_33" data-name="Group 33" transform="translate(4127 507)">
|
||||
<g id="Group_32" data-name="Group 32" transform="translate(0 0)">
|
||||
<g id="Group_31" data-name="Group 31" transform="translate(0 6.627)">
|
||||
<circle id="Ellipse_50" data-name="Ellipse 50" cx="3.066" cy="3.066" r="3.066" transform="translate(0 0)" fill="#383838"/>
|
||||
<circle id="Ellipse_51" data-name="Ellipse 51" cx="3.066" cy="3.066" r="3.066" transform="translate(7.664 0)" fill="#383838"/>
|
||||
</g>
|
||||
<circle id="Ellipse_52" data-name="Ellipse 52" cx="3.066" cy="3.066" r="3.066" transform="translate(3.826 0)" fill="#383838"/>
|
||||
<path id="Path_150" data-name="Path 150" d="M36.27,83.67" transform="translate(-33.199 -73.977)" fill="#ff0"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 973 B |
187
src/audio.ts
Normal file
187
src/audio.ts
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
/* eslint @typescript-eslint/no-var-requires: "off" */
|
||||
|
||||
"use strict";
|
||||
|
||||
// where the audio goes
|
||||
let buffer: ArrayBuffer[] = [];
|
||||
|
||||
// place audio data in buffer
|
||||
export const collect: IpcListenerCallback<ArrayBuffer> = (chunk) => {
|
||||
if (chunk) buffer.push(chunk);
|
||||
}
|
||||
|
||||
// return audio and clear buffer
|
||||
export const flush = async () => {
|
||||
const bufferCopy = buffer;
|
||||
buffer = [];
|
||||
return bufferCopy;
|
||||
}
|
||||
|
||||
// import { backgroundMitt } from '@/modules/emitter';
|
||||
// const portAudio = require('naudiodon');
|
||||
|
||||
// // Audio in and out stream objects.
|
||||
// let ai: typeof portAudio.AudioIO | boolean = false;
|
||||
// let ao: typeof portAudio.AudioIO | boolean = false;
|
||||
|
||||
// // Whether activly recording.
|
||||
// let record = false;
|
||||
|
||||
// const audioContainer = {
|
||||
// input: '',
|
||||
// }
|
||||
|
||||
// const audioOptions = {
|
||||
// channelCount: 1,
|
||||
// sampleFormat: 16,
|
||||
// sampleRate: 16000,
|
||||
// deviceId: -1,
|
||||
// closeOnError: false,
|
||||
// }
|
||||
|
||||
// export const toggleRecord = (): void => { record = !record };
|
||||
|
||||
|
||||
// export const fetchAudioInput = (): Promise<Error | string> => (
|
||||
|
||||
// new Promise((resolve, reject) => {
|
||||
|
||||
// try {
|
||||
// resolve(audioContainer.input);
|
||||
// toggleRecord();
|
||||
// } catch (e) {
|
||||
// reject(new Error('Failed to fetch the audio.'))
|
||||
// }
|
||||
|
||||
// })
|
||||
// )
|
||||
|
||||
|
||||
// // Main audio function run by run.ts module.
|
||||
// export function initAudioIO(): void {
|
||||
// console.log("AUDIO:Starting io streams.")
|
||||
|
||||
// if (!ai) {
|
||||
|
||||
// // Initialize and start input stream.
|
||||
// ai = new portAudio.AudioIO({ inOptions: audioOptions });
|
||||
// ai.setEncoding("hex");
|
||||
// ai.start();
|
||||
|
||||
// // On each data chunk...
|
||||
// ai.on('data', (chunk: string) => {
|
||||
|
||||
// // If recording, we capture the data.
|
||||
// if (record) {
|
||||
// console.log('AUDIO:Recording...')
|
||||
// audioContainer.input += chunk;
|
||||
// }
|
||||
|
||||
// // Else, we don't capture and also clear audioContainer.
|
||||
// else {
|
||||
// if (audioContainer.input.length) {
|
||||
// audioContainer.input = "";
|
||||
// }
|
||||
// }
|
||||
|
||||
// });
|
||||
// }
|
||||
|
||||
// if (!ao) {
|
||||
|
||||
// // Initialize and start input stream.
|
||||
// ao = new portAudio.AudioIO({ outOptions: audioOptions });
|
||||
// ao.start();
|
||||
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
// // ---Audio playback--------------------------------------------
|
||||
|
||||
// // Split Buffer into an array of len-sized Buffers.
|
||||
// function bufSplit(buf: Buffer, len: number): Array<Buffer> {
|
||||
// const chunks = [];
|
||||
// let i = 0;
|
||||
// let L = len;
|
||||
|
||||
// while(i < buf.byteLength) {
|
||||
// chunks.push(buf.slice(i, L));
|
||||
// i = L;
|
||||
// L += len;
|
||||
// }
|
||||
|
||||
// return chunks;
|
||||
// }
|
||||
|
||||
// // Audio playback.
|
||||
// export function play(input: string): void {
|
||||
|
||||
// // Format the audio.
|
||||
// const audio = bufSplit(
|
||||
// Buffer.from(input as string, 'hex'),
|
||||
// 8192
|
||||
// );
|
||||
|
||||
// // Called on end of write.
|
||||
// const callback = () => {
|
||||
|
||||
// // We stop audio playback anim.
|
||||
// backgroundMitt.emit('ipc-renderer', {
|
||||
// endpoint: 'stop-playback-anim'
|
||||
// });
|
||||
|
||||
// }
|
||||
|
||||
// write();
|
||||
|
||||
// // Iterate through audio array and write buffers to portAudio writable.
|
||||
// function write() {
|
||||
// let chunk: Buffer;
|
||||
// let ok = true;
|
||||
// let i = 0;
|
||||
|
||||
// do {
|
||||
// chunk = audio[i];
|
||||
// if (i === audio.length - 1) {
|
||||
// // write last chunk.
|
||||
// ao.write(chunk, null, callback);
|
||||
// } else {
|
||||
// // check for backpreassure.
|
||||
// ok = ao.write(chunk, null);
|
||||
// }
|
||||
// i++;
|
||||
// } while (i < audio.length && ok);
|
||||
|
||||
// if (i < audio.length) {
|
||||
// // Had to stop early!
|
||||
// // Write some more once it drains.
|
||||
// ao.once('drain', write);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// // -------------------------------------------------------------
|
||||
|
||||
// // Get's called on window close.
|
||||
// export async function stopStream() {
|
||||
// console.log("AUDIO:Stopping audio stream.")
|
||||
// if (ai) {
|
||||
// try {
|
||||
// await ai.quit()
|
||||
// } catch(e){
|
||||
// console.log('AUDIO: Failed to shutdown audio input.');
|
||||
// throw e;
|
||||
// }
|
||||
// }
|
||||
// if (ao) {
|
||||
// try {
|
||||
// await ao.quit()
|
||||
// } catch(e){
|
||||
// console.log('AUDIO: Failed to shutdown audio output.');
|
||||
// throw e;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
13
src/auth.ts
Normal file
13
src/auth.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
|
||||
export const parseAuthRes = (authRes: any) => {
|
||||
const token = authRes.headers['set-cookie'][0].split(";")[0].split("=")[1] as string;
|
||||
const profile = authRes.data as Profile;
|
||||
return {
|
||||
token,
|
||||
profile
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
/*
|
||||
* Entry point for Crimata electron app.
|
||||
* "Look on my Works, ye Mighty, and despair!"
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
import { 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);
|
||||
|
||||
})();
|
||||
|
|
@ -1,135 +0,0 @@
|
|||
/* eslint @typescript-eslint/no-var-requires: "off" */
|
||||
|
||||
"use strict";
|
||||
|
||||
import { sendAudio } from './session'
|
||||
import { ipcMain } from "electron";
|
||||
import { backgroundMitt } from './emitter';
|
||||
|
||||
const portAudio = require('naudiodon');
|
||||
|
||||
let ai: typeof portAudio.AudioIO | null = null;
|
||||
let ao: typeof portAudio.AudioIO | null = null;
|
||||
let record = false;
|
||||
const audioContainer = {
|
||||
input: '',
|
||||
}
|
||||
const encoding = "hex";
|
||||
const audioOptions = {
|
||||
channelCount: 1,
|
||||
sampleFormat: 16,
|
||||
sampleRate: 16000,
|
||||
deviceId: -1,
|
||||
closeOnError: false,
|
||||
}
|
||||
|
||||
// callback run on space bar key up and down.
|
||||
const updateRecorder = (_event, payload: {
|
||||
isRecording: boolean;
|
||||
uid: string | null;
|
||||
}): void => {
|
||||
record = payload.isRecording;
|
||||
if (!record) {
|
||||
if (payload.uid) sendAudio(audioContainer.input, payload.uid);
|
||||
}
|
||||
};
|
||||
|
||||
// listen for space key up/down event.
|
||||
ipcMain.removeAllListeners('update-recorder');
|
||||
ipcMain.on('update-recorder', updateRecorder);
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// main audio function run by run.ts module.
|
||||
export function initAudioIO(): void {
|
||||
|
||||
if (!ai) {
|
||||
ai = new portAudio.AudioIO({ inOptions: audioOptions });
|
||||
|
||||
// base64 encoding needed for google speech to text.
|
||||
ai.setEncoding(encoding);
|
||||
ai.start();
|
||||
|
||||
ai.on('data', (chunk: string) => {
|
||||
if (record) {
|
||||
console.log('recording...')
|
||||
audioContainer.input += chunk;
|
||||
} else {
|
||||
if (audioContainer.input.length) audioContainer.input = "";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!ao) {
|
||||
ao = new portAudio.AudioIO({ outOptions: audioOptions });
|
||||
ao.start();
|
||||
}
|
||||
}
|
||||
|
||||
// play audio buffers.
|
||||
export function play(input: Buffer): void {
|
||||
let i = 0;
|
||||
|
||||
const audio = bufSplit(input, 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;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function stopStream() {
|
||||
if(ai != null) {
|
||||
ai.quit();
|
||||
ai = null;
|
||||
}
|
||||
if (ao != null) {
|
||||
ao.quit();
|
||||
ao = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
/* eslint-disable */
|
||||
const EventEmitter = require('events');
|
||||
|
||||
class BackgroundMitt extends EventEmitter { }
|
||||
|
||||
export const backgroundMitt = new BackgroundMitt();
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
import { app } from "electron";
|
||||
import { main } from './run';
|
||||
import { backgroundMitt } from './emitter';
|
||||
import { stopStream } from './audio';
|
||||
|
||||
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") {
|
||||
stopStream();
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
import { createWindow } from './window';
|
||||
import { initSession } from './session';
|
||||
import { initAudioIO } from './audio';
|
||||
|
||||
let socket = false;
|
||||
|
||||
/*
|
||||
* The main function will be run after electron app is ready.
|
||||
*/
|
||||
export async function main() {
|
||||
|
||||
// create main window.
|
||||
await createWindow();
|
||||
|
||||
// Instantiate socket session with crimata-platorm.
|
||||
if (!socket) initSession();
|
||||
socket = true;
|
||||
|
||||
// Begin audio stream.
|
||||
initAudioIO();
|
||||
|
||||
}
|
||||
|
|
@ -1,198 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
import WebSocket from 'ws';
|
||||
import { backgroundMitt } from './emitter';
|
||||
import { ipcMain } from "electron";
|
||||
import { play } from './audio';
|
||||
|
||||
import {
|
||||
Creds,
|
||||
Profile,
|
||||
Annotation,
|
||||
StandardMessage,
|
||||
ClientMessage
|
||||
} from "@/types/message/index";
|
||||
|
||||
import { clientMessage, renderMessage } from '@/modules/message';
|
||||
|
||||
const ip = 'ws://127.0.0.1';
|
||||
const port = 8760;
|
||||
const reconnectTimeout = 3000; //ms
|
||||
let socket: WebSocket;
|
||||
let success = false;
|
||||
let auth = false;
|
||||
|
||||
const onAuthSession = async (e: any, payload: string | Creds) => {
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
console.log('Authenticating...');
|
||||
|
||||
// Handle auth response from server.
|
||||
backgroundMitt.once('auth-res', (res: string) => {
|
||||
|
||||
if (res == "locked") {
|
||||
reject(res);
|
||||
} else {
|
||||
console.log('Success!');
|
||||
auth = true;
|
||||
resolve(res);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
if (typeof payload !== "string") {
|
||||
payload = JSON.stringify(payload)
|
||||
}
|
||||
|
||||
// Send token to backend
|
||||
socket.send(payload);
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
//---Message Handlers-----------------------------------------------
|
||||
|
||||
const handleAuthMessage = (message: string) => {
|
||||
backgroundMitt.emit('auth-res', message);
|
||||
}
|
||||
|
||||
const handleProfileMessage = (message: Profile) => {
|
||||
backgroundMitt.emit('ipc-renderer', {
|
||||
endpoint: 'update-profile',
|
||||
message: message
|
||||
});
|
||||
}
|
||||
|
||||
const handleStandardMessage = (m: StandardMessage) => {
|
||||
|
||||
// Create a render message object.
|
||||
const message = renderMessage(
|
||||
m.content.text, m.content.audio, m.context, m.modifier);
|
||||
|
||||
// Play audio if any.
|
||||
if (message.content.audio) {
|
||||
const audioBytes = Buffer.from(m.content.audio as string, 'hex');
|
||||
m.content.audio = true;
|
||||
play(audioBytes);
|
||||
}
|
||||
|
||||
backgroundMitt.emit('ipc-renderer', {
|
||||
endpoint: 'render-message',
|
||||
message: message
|
||||
});
|
||||
}
|
||||
|
||||
const handleAnnotationMessage = (message: Annotation) => {
|
||||
backgroundMitt.emit('ipc-renderer', {
|
||||
endpoint: 'render-message',
|
||||
message: message
|
||||
});
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------
|
||||
|
||||
const onMessage = (messageStr: string): void => {
|
||||
|
||||
// Auth messages are strings.
|
||||
if (!auth) {
|
||||
handleAuthMessage(messageStr)
|
||||
return
|
||||
}
|
||||
|
||||
const message = JSON.parse(messageStr)
|
||||
console.log("New message:")
|
||||
console.log(message)
|
||||
|
||||
if (message.content) {
|
||||
handleStandardMessage(message)
|
||||
}
|
||||
|
||||
else if (message.first) {
|
||||
handleProfileMessage(message)
|
||||
}
|
||||
|
||||
else {
|
||||
handleAnnotationMessage(message)
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
const onClientMessage = (e: any, payload: ClientMessage): void => {
|
||||
console.log('sending message');
|
||||
socket.send(JSON.stringify(payload));
|
||||
}
|
||||
|
||||
const restart = () => {
|
||||
auth = false;
|
||||
initSession();
|
||||
}
|
||||
|
||||
// Run every time we want to connect to backend.
|
||||
export function initSession(): void {
|
||||
|
||||
// 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', onAuthSession);
|
||||
|
||||
// handle user message event
|
||||
ipcMain.removeAllListeners('client-message');
|
||||
ipcMain.on('client-message', onClientMessage);
|
||||
|
||||
// handle renderer logout event
|
||||
ipcMain.removeAllListeners('logout');
|
||||
ipcMain.on('logout', (_event, _payload: string) => restart());
|
||||
|
||||
// Connect to the backend.
|
||||
socket.on('open', () => {
|
||||
console.log('Connected to Crimata Servers.');
|
||||
|
||||
// Try to authenticate token right away.
|
||||
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...');
|
||||
restart();
|
||||
});
|
||||
|
||||
socket.on("message", onMessage);
|
||||
}
|
||||
|
||||
export function sendAudio(audio: string, uid: string): void {
|
||||
const m = clientMessage("", audio, uid);
|
||||
socket.send(JSON.stringify(m));
|
||||
}
|
||||
|
|
@ -1,105 +0,0 @@
|
|||
import anime from "animejs";
|
||||
import { onMounted, onUnmounted, ref, Ref } from "vue";
|
||||
import useMitt from "@/modules/mitt";
|
||||
import { useIpc } from '@/modules/ipc';
|
||||
import keyboardNameMap from "../keyBoardMaps/keyboardNameMap";
|
||||
import { renderMessage } from '@/modules/message';
|
||||
|
||||
|
||||
function showRecIcon () {
|
||||
|
||||
anime({
|
||||
targets: '#recIcon',
|
||||
opacity: [0, 0.75],
|
||||
scale: [0.0, 1],
|
||||
duration: 250,
|
||||
easing: 'linear',
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
function hideRecIcon () {
|
||||
|
||||
anime({
|
||||
targets: '#recIcon',
|
||||
opacity: [0.75, 0],
|
||||
scale: [1, 0],
|
||||
duration: 250,
|
||||
easing: 'linear',
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
|
||||
export default function useAudioInputController (typing: Ref) {
|
||||
|
||||
// For sending messages.
|
||||
const { post } = useIpc();
|
||||
const { emitter } = useMitt();
|
||||
|
||||
// Keepp track of when we are recording.
|
||||
const recording = ref(false);
|
||||
|
||||
//---Callbacks-----------------------------------------------
|
||||
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
const cmd = keyboardNameMap[e.keyCode];
|
||||
// console.log(cmd)
|
||||
|
||||
// Start recording on space bar.
|
||||
if (cmd == "SPACE" && !recording.value && !typing.value) {
|
||||
|
||||
post('update-recorder', {
|
||||
isRecording: true,
|
||||
uid: null
|
||||
});
|
||||
|
||||
showRecIcon()
|
||||
recording.value = true;
|
||||
console.log("Recording...")
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const onKeyUp = (e: KeyboardEvent) => {
|
||||
const cmd = keyboardNameMap[e.keyCode];
|
||||
|
||||
// Stop recording on space up.
|
||||
if (cmd == "SPACE" && recording.value) {
|
||||
|
||||
// Render audio immediately.
|
||||
const message = renderMessage("", "", "", "sf")
|
||||
emitter.emit("self-message", message);
|
||||
|
||||
post('update-recorder', {
|
||||
isRecording: false,
|
||||
uid: message.uid
|
||||
});
|
||||
|
||||
hideRecIcon()
|
||||
recording.value = false;
|
||||
console.log("Stopping record...")
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener("keydown", onKeyDown);
|
||||
window.addEventListener("keyup", onKeyUp);
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener("keydown", onKeyDown);
|
||||
window.removeEventListener("keyup", onKeyUp);
|
||||
});
|
||||
|
||||
|
||||
return {
|
||||
recording
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,174 +0,0 @@
|
|||
import anime from "animejs";
|
||||
import useMitt from "@/modules/mitt";
|
||||
import { useIpc } from '@/modules/ipc';
|
||||
import { Ref, ref, watch, onMounted, onUnmounted } from "vue";
|
||||
import keyboardNameMap from "../keyBoardMaps/keyboardNameMap";
|
||||
import { clientMessage, renderMessage } from '@/modules/message';
|
||||
|
||||
//---Animations-----------------------------------------------
|
||||
|
||||
let side = "right"; // Side of parent we are on.
|
||||
|
||||
function showTextInput () {
|
||||
const t1 = (side === "right") ? 50 : -70;
|
||||
const t2 = (side === "right") ? 110 : -130;
|
||||
|
||||
anime({
|
||||
targets: '#textInput',
|
||||
opacity: [0, 1],
|
||||
translateX: [t1, t2],
|
||||
scale: [0.3, 1],
|
||||
duration: 500,
|
||||
easing: 'easeOutExpo',
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
function hideTextInput() {
|
||||
const t = (side === "right") ? 50 : -80;
|
||||
|
||||
anime({
|
||||
targets: '#textInput',
|
||||
opacity: [1, 0],
|
||||
translateX: t,
|
||||
scale: 0.3,
|
||||
duration: 500,
|
||||
easing: 'easeOutExpo',
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
function switchSide(currentSide: string) {
|
||||
const t = (currentSide === "right") ? -130 : 110;
|
||||
|
||||
anime({
|
||||
targets: '#textInput',
|
||||
translateX: t,
|
||||
duration: 500,
|
||||
easing: 'easeOutExpo',
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
//------------------------------------------------------------
|
||||
|
||||
|
||||
export default function useTextInputController(elementX: Ref) {
|
||||
let textInput: HTMLInputElement | null;
|
||||
|
||||
const { post } = useIpc();
|
||||
const { emitter } = useMitt();
|
||||
|
||||
let firstKey = true;
|
||||
const typing = ref(false);
|
||||
|
||||
// Prep inputItem for typing.
|
||||
const prepInput = () => {
|
||||
showTextInput()
|
||||
typing.value = true
|
||||
}
|
||||
|
||||
// Clear and hide inputItem after done typing.
|
||||
const clearInput = () => {
|
||||
|
||||
if (textInput) {
|
||||
textInput.value = "";
|
||||
textInput.blur();
|
||||
}
|
||||
|
||||
hideTextInput()
|
||||
firstKey = true;
|
||||
typing.value = false;
|
||||
}
|
||||
|
||||
// Send a message and clean up after.
|
||||
const sendMessage = () => {
|
||||
if (textInput) {
|
||||
|
||||
// Render message
|
||||
const message = renderMessage(textInput.value, false, "", "sf")
|
||||
emitter.emit("self-message", message);
|
||||
|
||||
// Send it to the backend for processing.
|
||||
const clientM = clientMessage(textInput.value, false, message.uid);
|
||||
post('client-message', clientM);
|
||||
|
||||
clearInput()
|
||||
}
|
||||
}
|
||||
|
||||
//---Callbacks-----------------------------------------------
|
||||
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
const key = keyboardNameMap[e.keyCode]
|
||||
|
||||
if (textInput) {
|
||||
|
||||
// First-key handler.
|
||||
if (firstKey) {
|
||||
if (key == "SPACE") return
|
||||
prepInput()
|
||||
}
|
||||
|
||||
textInput.focus();
|
||||
|
||||
// Close input when no text or on ESC.
|
||||
if (textInput.value == "" && !firstKey) {
|
||||
clearInput()
|
||||
return
|
||||
}
|
||||
|
||||
if (key === "ESCAPE") {
|
||||
clearInput()
|
||||
return
|
||||
}
|
||||
|
||||
// Close and send on enter.
|
||||
if (key === "ENTER") {
|
||||
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 === "right") {
|
||||
if (winW - elementX < 230) {
|
||||
switchSide(side)
|
||||
side = "left"
|
||||
}
|
||||
}
|
||||
|
||||
else {
|
||||
if (winW - elementX > 230) {
|
||||
switchSide(side)
|
||||
side = "right"
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
textInput = document.getElementById("textInput") as HTMLInputElement;
|
||||
window.addEventListener("keydown", onKeyDown);
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener("keydown", onKeyDown);
|
||||
});
|
||||
|
||||
return {
|
||||
typing
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,263 +0,0 @@
|
|||
// This has the UnShifted and Shifted characters that each key maps to
|
||||
// Ones that are to be ignored for character input are empty.
|
||||
const keyboardCharMap = [
|
||||
["", ""], // [0]
|
||||
["", ""], // [1]
|
||||
["", ""], // [2]
|
||||
["", ""], // [3]
|
||||
["", ""], // [4]
|
||||
["", ""], // [5]
|
||||
["", ""], // [6]
|
||||
["", ""], // [7]
|
||||
["", ""], // [8]
|
||||
["", ""], // [9]
|
||||
["", ""], // [10]
|
||||
["", ""], // [11]
|
||||
["", ""], // [12]
|
||||
["\r", "\r"], // [13] - MOST control characters are ignored. This one (Carriage Return, or "Enter") is significant!
|
||||
["", ""], // [14]
|
||||
["", ""], // [15]
|
||||
["", ""], // [16]
|
||||
["", ""], // [17]
|
||||
["", ""], // [18]
|
||||
["", ""], // [19]
|
||||
["", ""], // [20]
|
||||
["", ""], // [21]
|
||||
["", ""], // [22]
|
||||
["", ""], // [23]
|
||||
["", ""], // [24]
|
||||
["", ""], // [25]
|
||||
["", ""], // [26]
|
||||
["", ""], // [27]
|
||||
["", ""], // [28]
|
||||
["", ""], // [29]
|
||||
["", ""], // [30]
|
||||
["", ""], // [31]
|
||||
[" ", " "], // [32] // SPACE! Don't "clean it up" and remove the space!
|
||||
["", ""], // [33]
|
||||
["", ""], // [34]
|
||||
["", ""], // [35]
|
||||
["", ""], // [36]
|
||||
["", ""], // [37]
|
||||
["", ""], // [38]
|
||||
["", ""], // [39]
|
||||
["", ""], // [40]
|
||||
["", ""], // [41]
|
||||
["", ""], // [42]
|
||||
["", ""], // [43]
|
||||
["", ""], // [44]
|
||||
["", ""], // [45]
|
||||
["", ""], // [46]
|
||||
["", ""], // [47]
|
||||
["0", ")"], // [48]
|
||||
["1", "!"], // [49]
|
||||
["2", "@"], // [50]
|
||||
["3", "#"], // [51]
|
||||
["4", "$"], // [52]
|
||||
["5", "%"], // [53]
|
||||
["6", "^"], // [54]
|
||||
["7", "&"], // [55]
|
||||
["8", "*"], // [56]
|
||||
["9", "("], // [57]
|
||||
["", ""], // [58]
|
||||
[";", ":"], // [59]
|
||||
["<", ""], // [60]
|
||||
["=", ""], // [61]
|
||||
[">", ""], // [62]
|
||||
["?", ""], // [63] shifted; else "/"
|
||||
["", ""], // [64]
|
||||
["a", "A"], // [65]
|
||||
["b", "B"], // [66]
|
||||
["c", "C"], // [67]
|
||||
["d", "D"], // [68]
|
||||
["e", "E"], // [69]
|
||||
["f", "F"], // [70]
|
||||
["g", "G"], // [71]
|
||||
["h", "H"], // [72]
|
||||
["i", "I"], // [73]
|
||||
["j", "J"], // [74]
|
||||
["k", "K"], // [75]
|
||||
["l", "L"], // [76]
|
||||
["m", "M"], // [77]
|
||||
["n", "N"], // [78]
|
||||
["o", "O"], // [79]
|
||||
["p", "P"], // [80]
|
||||
["q", "Q"], // [81]
|
||||
["r", "R"], // [82]
|
||||
["s", "S"], // [83]
|
||||
["t", "T"], // [84]
|
||||
["u", "U"], // [85]
|
||||
["v", "V"], // [86]
|
||||
["w", "W"], // [87]
|
||||
["x", "X"], // [88]
|
||||
["y", "Y"], // [89]
|
||||
["z", "Z"], // [90]
|
||||
["", ""], // [91] Windows Key (Windows) or Command Key (Mac)
|
||||
["", ""], // [92]
|
||||
["", ""], // [93]
|
||||
["", ""], // [94]
|
||||
["", ""], // [95]
|
||||
// Number Keypad Entries...
|
||||
["0", ""], // [96]
|
||||
["1", ""], // [97]
|
||||
["2", ""], // [98]
|
||||
["3", ""], // [99]
|
||||
["4", ""], // [100]
|
||||
["5", ""], // [101]
|
||||
["6", ""], // [102]
|
||||
["7", ""], // [103]
|
||||
["8", ""], // [104]
|
||||
["9", ""], // [105]
|
||||
["*", ""], // [106]
|
||||
["+", ""], // [107]
|
||||
["", ""], // [108]
|
||||
["-", ""], // [109]
|
||||
[".", ""], // [110]
|
||||
["/", ""], // [111]
|
||||
|
||||
["", ""], // [112]
|
||||
["", ""], // [113]
|
||||
["", ""], // [114]
|
||||
["", ""], // [115]
|
||||
["", ""], // [116]
|
||||
["", ""], // [117]
|
||||
["", ""], // [118]
|
||||
["", ""], // [119]
|
||||
["", ""], // [120]
|
||||
["", ""], // [121]
|
||||
["", ""], // [122]
|
||||
["", ""], // [123]
|
||||
["", ""], // [124]
|
||||
["", ""], // [125]
|
||||
["", ""], // [126]
|
||||
["", ""], // [127]
|
||||
["", ""], // [128]
|
||||
["", ""], // [129]
|
||||
["", ""], // [130]
|
||||
["", ""], // [131]
|
||||
["", ""], // [132]
|
||||
["", ""], // [133]
|
||||
["", ""], // [134]
|
||||
["", ""], // [135]
|
||||
["", ""], // [136]
|
||||
["", ""], // [137]
|
||||
["", ""], // [138]
|
||||
["", ""], // [139]
|
||||
["", ""], // [140]
|
||||
["", ""], // [141]
|
||||
["", ""], // [142]
|
||||
["", ""], // [143]
|
||||
["", ""], // [144]
|
||||
["", ""], // [145]
|
||||
["", ""], // [146]
|
||||
["", ""], // [147]
|
||||
["", ""], // [148]
|
||||
["", ""], // [149]
|
||||
["", ""], // [150]
|
||||
["", ""], // [151]
|
||||
["", ""], // [152]
|
||||
["", ""], // [153]
|
||||
["", ""], // [154]
|
||||
["", ""], // [155]
|
||||
["", ""], // [156]
|
||||
["", ""], // [157]
|
||||
["", ""], // [158]
|
||||
["", ""], // [159]
|
||||
["", ""], // [160]
|
||||
["", ""], // [161]
|
||||
["", ""], // [162]
|
||||
["", ""], // [163]
|
||||
["", ""], // [164]
|
||||
["", ""], // [165]
|
||||
["", ""], // [166]
|
||||
["", ""], // [167]
|
||||
["", ""], // [168]
|
||||
["", ""], // [169]
|
||||
["", ""], // [170]
|
||||
["", ""], // [171]
|
||||
["", ""], // [172]
|
||||
["", ""], // [173]
|
||||
["", ""], // [174]
|
||||
["", ""], // [175]
|
||||
["", ""], // [176]
|
||||
["", ""], // [177]
|
||||
["", ""], // [178]
|
||||
["", ""], // [179]
|
||||
["", ""], // [180]
|
||||
["", ""], // [181]
|
||||
["", ""], // [182]
|
||||
["", ""], // [183]
|
||||
["", ""], // [184]
|
||||
["", ""], // [185]
|
||||
[";", ":"], // [186]
|
||||
["=", "+"], // [187]
|
||||
[",", "<"], // [188]
|
||||
["-", "_"], // [189]
|
||||
[".", ">"], // [190]
|
||||
["/", "?"], // [191]
|
||||
["`", "~"], // [192]
|
||||
["", ""], // [193]
|
||||
["", ""], // [194]
|
||||
["", ""], // [195]
|
||||
["", ""], // [196]
|
||||
["", ""], // [197]
|
||||
["", ""], // [198]
|
||||
["", ""], // [199]
|
||||
["", ""], // [200]
|
||||
["", ""], // [201]
|
||||
["", ""], // [202]
|
||||
["", ""], // [203]
|
||||
["", ""], // [204]
|
||||
["", ""], // [205]
|
||||
["", ""], // [206]
|
||||
["", ""], // [207]
|
||||
["", ""], // [208]
|
||||
["", ""], // [209]
|
||||
["", ""], // [210]
|
||||
["", ""], // [211]
|
||||
["", ""], // [212]
|
||||
["", ""], // [213]
|
||||
["", ""], // [214]
|
||||
["", ""], // [215]
|
||||
["", ""], // [216]
|
||||
["", ""], // [217]
|
||||
["", ""], // [218]
|
||||
["[", "{"], // [219]
|
||||
["\\", "|"], // [220]
|
||||
["]", "}"], // [221]
|
||||
["'", '"'], // [222]
|
||||
["", ""], // [223]
|
||||
["", ""], // [224]
|
||||
["", ""], // [225]
|
||||
["", ""], // [226]
|
||||
["", ""], // [227]
|
||||
["", ""], // [228]
|
||||
["", ""], // [229]
|
||||
["", ""], // [230]
|
||||
["", ""], // [231]
|
||||
["", ""], // [232]
|
||||
["", ""], // [233]
|
||||
["", ""], // [234]
|
||||
["", ""], // [235]
|
||||
["", ""], // [236]
|
||||
["", ""], // [237]
|
||||
["", ""], // [238]
|
||||
["", ""], // [239]
|
||||
["", ""], // [240]
|
||||
["", ""], // [241]
|
||||
["", ""], // [242]
|
||||
["", ""], // [243]
|
||||
["", ""], // [244]
|
||||
["", ""], // [245]
|
||||
["", ""], // [246]
|
||||
["", ""], // [247]
|
||||
["", ""], // [248]
|
||||
["", ""], // [249]
|
||||
["", ""], // [250]
|
||||
["", ""], // [251]
|
||||
["", ""], // [252]
|
||||
["", ""], // [253]
|
||||
["", ""], // [254]
|
||||
["", ""] // [255]
|
||||
];
|
||||
export default keyboardCharMap;
|
||||
|
|
@ -1,261 +0,0 @@
|
|||
// names of known key codes (0-255)
|
||||
const keyboardNameMap = [
|
||||
"", // [0]
|
||||
"", // [1]
|
||||
"", // [2]
|
||||
"CANCEL", // [3]
|
||||
"", // [4]
|
||||
"", // [5]
|
||||
"HELP", // [6]
|
||||
"", // [7]
|
||||
"BACK_SPACE", // [8]
|
||||
"TAB", // [9]
|
||||
"", // [10]
|
||||
"", // [11]
|
||||
"CLEAR", // [12]
|
||||
"ENTER", // [13]
|
||||
"ENTER_SPECIAL", // [14]
|
||||
"", // [15]
|
||||
"SHIFT", // [16]
|
||||
"CONTROL", // [17]
|
||||
"ALT", // [18]
|
||||
"PAUSE", // [19]
|
||||
"CAPS_LOCK", // [20]
|
||||
"KANA", // [21]
|
||||
"EISU", // [22]
|
||||
"JUNJA", // [23]
|
||||
"FINAL", // [24]
|
||||
"HANJA", // [25]
|
||||
"", // [26]
|
||||
"ESCAPE", // [27]
|
||||
"CONVERT", // [28]
|
||||
"NONCONVERT", // [29]
|
||||
"ACCEPT", // [30]
|
||||
"MODECHANGE", // [31]
|
||||
"SPACE", // [32]
|
||||
"PAGE_UP", // [33]
|
||||
"PAGE_DOWN", // [34]
|
||||
"END", // [35]
|
||||
"HOME", // [36]
|
||||
"LEFT", // [37]
|
||||
"UP", // [38]
|
||||
"RIGHT", // [39]
|
||||
"DOWN", // [40]
|
||||
"SELECT", // [41]
|
||||
"PRINT", // [42]
|
||||
"EXECUTE", // [43]
|
||||
"PRINTSCREEN", // [44]
|
||||
"INSERT", // [45]
|
||||
"DELETE", // [46]
|
||||
"", // [47]
|
||||
"0", // [48]
|
||||
"1", // [49]
|
||||
"2", // [50]
|
||||
"3", // [51]
|
||||
"4", // [52]
|
||||
"5", // [53]
|
||||
"6", // [54]
|
||||
"7", // [55]
|
||||
"8", // [56]
|
||||
"9", // [57]
|
||||
"COLON", // [58]
|
||||
"SEMICOLON", // [59]
|
||||
"LESS_THAN", // [60]
|
||||
"EQUALS", // [61]
|
||||
"GREATER_THAN", // [62]
|
||||
"QUESTION_MARK", // [63]
|
||||
"AT", // [64]
|
||||
"A", // [65]
|
||||
"B", // [66]
|
||||
"C", // [67]
|
||||
"D", // [68]
|
||||
"E", // [69]
|
||||
"F", // [70]
|
||||
"G", // [71]
|
||||
"H", // [72]
|
||||
"I", // [73]
|
||||
"J", // [74]
|
||||
"K", // [75]
|
||||
"L", // [76]
|
||||
"M", // [77]
|
||||
"N", // [78]
|
||||
"O", // [79]
|
||||
"P", // [80]
|
||||
"Q", // [81]
|
||||
"R", // [82]
|
||||
"S", // [83]
|
||||
"T", // [84]
|
||||
"U", // [85]
|
||||
"V", // [86]
|
||||
"W", // [87]
|
||||
"X", // [88]
|
||||
"Y", // [89]
|
||||
"Z", // [90]
|
||||
"OS_KEY", // [91] Windows Key (Windows) or Command Key (Mac)
|
||||
"", // [92]
|
||||
"CONTEXT_MENU", // [93]
|
||||
"", // [94]
|
||||
"SLEEP", // [95]
|
||||
"NUMPAD0", // [96]
|
||||
"NUMPAD1", // [97]
|
||||
"NUMPAD2", // [98]
|
||||
"NUMPAD3", // [99]
|
||||
"NUMPAD4", // [100]
|
||||
"NUMPAD5", // [101]
|
||||
"NUMPAD6", // [102]
|
||||
"NUMPAD7", // [103]
|
||||
"NUMPAD8", // [104]
|
||||
"NUMPAD9", // [105]
|
||||
"MULTIPLY", // [106]
|
||||
"ADD", // [107]
|
||||
"SEPARATOR", // [108]
|
||||
"SUBTRACT", // [109]
|
||||
"DECIMAL", // [110]
|
||||
"DIVIDE", // [111]
|
||||
"F1", // [112]
|
||||
"F2", // [113]
|
||||
"F3", // [114]
|
||||
"F4", // [115]
|
||||
"F5", // [116]
|
||||
"F6", // [117]
|
||||
"F7", // [118]
|
||||
"F8", // [119]
|
||||
"F9", // [120]
|
||||
"F10", // [121]
|
||||
"F11", // [122]
|
||||
"F12", // [123]
|
||||
"F13", // [124]
|
||||
"F14", // [125]
|
||||
"F15", // [126]
|
||||
"F16", // [127]
|
||||
"F17", // [128]
|
||||
"F18", // [129]
|
||||
"F19", // [130]
|
||||
"F20", // [131]
|
||||
"F21", // [132]
|
||||
"F22", // [133]
|
||||
"F23", // [134]
|
||||
"F24", // [135]
|
||||
"", // [136]
|
||||
"", // [137]
|
||||
"", // [138]
|
||||
"", // [139]
|
||||
"", // [140]
|
||||
"", // [141]
|
||||
"", // [142]
|
||||
"", // [143]
|
||||
"NUM_LOCK", // [144]
|
||||
"SCROLL_LOCK", // [145]
|
||||
"WIN_OEM_FJ_JISHO", // [146]
|
||||
"WIN_OEM_FJ_MASSHOU", // [147]
|
||||
"WIN_OEM_FJ_TOUROKU", // [148]
|
||||
"WIN_OEM_FJ_LOYA", // [149]
|
||||
"WIN_OEM_FJ_ROYA", // [150]
|
||||
"", // [151]
|
||||
"", // [152]
|
||||
"", // [153]
|
||||
"", // [154]
|
||||
"", // [155]
|
||||
"", // [156]
|
||||
"", // [157]
|
||||
"", // [158]
|
||||
"", // [159]
|
||||
"CIRCUMFLEX", // [160]
|
||||
"EXCLAMATION", // [161]
|
||||
"DOUBLE_QUOTE", // [162]
|
||||
"HASH", // [163]
|
||||
"DOLLAR", // [164]
|
||||
"PERCENT", // [165]
|
||||
"AMPERSAND", // [166]
|
||||
"UNDERSCORE", // [167]
|
||||
"OPEN_PAREN", // [168]
|
||||
"CLOSE_PAREN", // [169]
|
||||
"ASTERISK", // [170]
|
||||
"PLUS", // [171]
|
||||
"PIPE", // [172]
|
||||
"HYPHEN_MINUS", // [173]
|
||||
"OPEN_CURLY_BRACKET", // [174]
|
||||
"CLOSE_CURLY_BRACKET", // [175]
|
||||
"TILDE", // [176]
|
||||
"", // [177]
|
||||
"", // [178]
|
||||
"", // [179]
|
||||
"", // [180]
|
||||
"VOLUME_MUTE", // [181]
|
||||
"VOLUME_DOWN", // [182]
|
||||
"VOLUME_UP", // [183]
|
||||
"", // [184]
|
||||
"", // [185]
|
||||
"SEMICOLON", // [186]
|
||||
"EQUALS", // [187]
|
||||
"COMMA", // [188]
|
||||
"MINUS", // [189]
|
||||
"PERIOD", // [190]
|
||||
"SLASH", // [191]
|
||||
"BACK_QUOTE", // [192]
|
||||
"", // [193]
|
||||
"", // [194]
|
||||
"", // [195]
|
||||
"", // [196]
|
||||
"", // [197]
|
||||
"", // [198]
|
||||
"", // [199]
|
||||
"", // [200]
|
||||
"", // [201]
|
||||
"", // [202]
|
||||
"", // [203]
|
||||
"", // [204]
|
||||
"", // [205]
|
||||
"", // [206]
|
||||
"", // [207]
|
||||
"", // [208]
|
||||
"", // [209]
|
||||
"", // [210]
|
||||
"", // [211]
|
||||
"", // [212]
|
||||
"", // [213]
|
||||
"", // [214]
|
||||
"", // [215]
|
||||
"", // [216]
|
||||
"", // [217]
|
||||
"", // [218]
|
||||
"OPEN_BRACKET", // [219]
|
||||
"BACK_SLASH", // [220]
|
||||
"CLOSE_BRACKET", // [221]
|
||||
"QUOTE", // [222]
|
||||
"", // [223]
|
||||
"META", // [224]
|
||||
"ALTGR", // [225]
|
||||
"", // [226]
|
||||
"WIN_ICO_HELP", // [227]
|
||||
"WIN_ICO_00", // [228]
|
||||
"", // [229]
|
||||
"WIN_ICO_CLEAR", // [230]
|
||||
"", // [231]
|
||||
"", // [232]
|
||||
"WIN_OEM_RESET", // [233]
|
||||
"WIN_OEM_JUMP", // [234]
|
||||
"WIN_OEM_PA1", // [235]
|
||||
"WIN_OEM_PA2", // [236]
|
||||
"WIN_OEM_PA3", // [237]
|
||||
"WIN_OEM_WSCTRL", // [238]
|
||||
"WIN_OEM_CUSEL", // [239]
|
||||
"WIN_OEM_ATTN", // [240]
|
||||
"WIN_OEM_FINISH", // [241]
|
||||
"WIN_OEM_COPY", // [242]
|
||||
"WIN_OEM_AUTO", // [243]
|
||||
"WIN_OEM_ENLW", // [244]
|
||||
"WIN_OEM_BACKTAB", // [245]
|
||||
"ATTN", // [246]
|
||||
"CRSEL", // [247]
|
||||
"EXSEL", // [248]
|
||||
"EREOF", // [249]
|
||||
"PLAY", // [250]
|
||||
"ZOOM", // [251]
|
||||
"", // [252]
|
||||
"PA1", // [253]
|
||||
"WIN_OEM_CLEAR", // [254]
|
||||
"" // [255]
|
||||
];
|
||||
|
||||
export default keyboardNameMap;
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
<template>
|
||||
|
||||
<input
|
||||
id="textInput"
|
||||
type="text"
|
||||
/>
|
||||
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "TextInput",
|
||||
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
#textInput {
|
||||
position: absolute;
|
||||
|
||||
opacity: 0;
|
||||
|
||||
min-width: 150px;
|
||||
height: 16px;
|
||||
|
||||
border-radius: 18px;
|
||||
|
||||
padding: 10px;
|
||||
margin-right: 10px;
|
||||
margin-left: 10px;
|
||||
|
||||
outline: none;
|
||||
border: none;
|
||||
pointer-events: none;
|
||||
|
||||
background-color: white;
|
||||
|
||||
z-index: -1;
|
||||
|
||||
box-shadow: 0px 0px 10px 0px rgba(0,0,0,0.15);
|
||||
|
||||
transform: translateX(50px) scale(0.3);
|
||||
|
||||
}
|
||||
|
||||
</style>
|
||||
31
src/composables/autoUpdate.ts
Normal file
31
src/composables/autoUpdate.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
const { autoUpdater } = require('electron-updater');
|
||||
|
||||
let win: boolean;
|
||||
|
||||
// Listen for window creation.
|
||||
backgroundMitt.on('window-active', (state: boolean) => {
|
||||
win = state;
|
||||
});
|
||||
|
||||
// Auto updating.
|
||||
autoUpdater.requestHeaders = { 'PRIVATE-TOKEN': 'mvvgWYwWnot4bisiQMh_' }
|
||||
|
||||
autoUpdater.on('update-available', (info: any) => {
|
||||
console.log(`Update available: ${info.version}`)
|
||||
})
|
||||
|
||||
autoUpdater.on('update-downloaded', (info: any) => {
|
||||
|
||||
const updateDialog = {
|
||||
type: 'info',
|
||||
buttons: ['Restart', 'Later'],
|
||||
title: 'Application Update',
|
||||
message: info.version,
|
||||
detail: 'A new version has been downloaded. Restart the application to apply the updates.'
|
||||
}
|
||||
|
||||
dialog.showMessageBox(updateDialog).then((returnValue) => {
|
||||
if (returnValue.response === 0) autoUpdater.quitAndInstall()
|
||||
})
|
||||
|
||||
})
|
||||
16
src/composables/useEmitter.ts
Normal file
16
src/composables/useEmitter.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
|
||||
// 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
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
51
src/composables/useHttp.ts
Normal file
51
src/composables/useHttp.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
|
||||
import axios, { AxiosRequestConfig } from 'axios';
|
||||
import {config} from "@/config";
|
||||
|
||||
const baseURL = config.BUSINESS_URL + config.BUSINESS_PREFIX;
|
||||
|
||||
interface Request {
|
||||
endpoint: string;
|
||||
query?: Record<string, any>;
|
||||
config?: Record<string, any>;
|
||||
}
|
||||
|
||||
const makeQuery = (reqQuery: Record<string, any>) => {
|
||||
|
||||
let result = '';
|
||||
|
||||
result = '?' + Object.entries(reqQuery)
|
||||
.map(([ key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
|
||||
.join('&')
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
|
||||
export default function useHttp() {
|
||||
|
||||
const api = axios.create({
|
||||
baseURL,
|
||||
withCredentials: true,
|
||||
});
|
||||
|
||||
|
||||
const post = async (endpoint: string, payload?: Record<string, any>): Promise<any> => (
|
||||
await api.post(endpoint, payload)
|
||||
)
|
||||
|
||||
|
||||
const get = async (req: Request) => {
|
||||
|
||||
if (req.query) {
|
||||
req.endpoint += makeQuery(req.query);
|
||||
}
|
||||
|
||||
const res = await api.get(req.endpoint, req.config);
|
||||
return res;
|
||||
};
|
||||
|
||||
return {
|
||||
get, post
|
||||
}
|
||||
}
|
||||
82
src/composables/useIpcMain.ts
Normal file
82
src/composables/useIpcMain.ts
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
|
||||
import { ipcMain, IpcMainInvokeEvent, IpcMainEvent } from "electron";
|
||||
|
||||
export class IpcHandler<InputParam, ReturnType> implements IIpcHandler<InputParam, ReturnType> {
|
||||
|
||||
readonly channel: string;
|
||||
|
||||
readonly _handlerCallback: IpcHandlerCallback<InputParam, ReturnType>;
|
||||
|
||||
constructor(options: {
|
||||
channel: string;
|
||||
handlerCallback: IpcHandlerCallback<InputParam, ReturnType>;
|
||||
}) {
|
||||
this.channel = options.channel;
|
||||
this._handlerCallback = options.handlerCallback;
|
||||
}
|
||||
|
||||
handle() {
|
||||
this.remove();
|
||||
ipcMain.handle(this.channel, this._onInvoke);
|
||||
}
|
||||
|
||||
remove() {
|
||||
ipcMain.removeHandler(this.channel);
|
||||
}
|
||||
|
||||
private _onInvoke = (_e: IpcMainInvokeEvent, payload?: string | null): Promise<Error | ReturnType> => {
|
||||
|
||||
return new Promise(async (resolve, reject) => {
|
||||
|
||||
console.log(`[IPC] Handle:${this.channel}`);
|
||||
|
||||
try {
|
||||
|
||||
const params = payload ? JSON.parse(payload) : null;
|
||||
|
||||
const res = await this._handlerCallback(params);
|
||||
|
||||
resolve(res as unknown as ReturnType);
|
||||
|
||||
} catch(e) {
|
||||
console.log(`[IPC] Error:${this.channel}`);
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
export class IpcListener<InputParam> implements IIpcListener<InputParam> {
|
||||
|
||||
readonly channel: string;
|
||||
|
||||
readonly _listenerCallback: IpcListenerCallback<InputParam>;
|
||||
|
||||
constructor(options: {
|
||||
channel: string;
|
||||
listenerCallback: IpcListenerCallback<InputParam>;
|
||||
}) {
|
||||
this.channel = options.channel;
|
||||
this._listenerCallback = options.listenerCallback;
|
||||
}
|
||||
|
||||
listen() {
|
||||
this.remove();
|
||||
ipcMain.on(this.channel, this._onPost);
|
||||
}
|
||||
|
||||
remove() {
|
||||
ipcMain.removeAllListeners(this.channel);
|
||||
}
|
||||
|
||||
private _onPost = (_e: IpcMainEvent, payload?: string | null): void => {
|
||||
|
||||
console.log(`[IPC] Post: ${this.channel}`);
|
||||
|
||||
const params = payload ? JSON.parse(payload) : null;
|
||||
this._listenerCallback(params);
|
||||
}
|
||||
|
||||
}
|
||||
36
src/composables/useMessageCanvas.ts
Normal file
36
src/composables/useMessageCanvas.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
|
||||
|
||||
|
||||
export default class Canvas {
|
||||
|
||||
messages: Message[];
|
||||
|
||||
/* seed canvas with messages on init */
|
||||
constructor(messages: Message[]) {
|
||||
this.messages = messages;
|
||||
ipcEmit("seed-view", this.messages);
|
||||
}
|
||||
|
||||
/* add a new message to the canvas */
|
||||
add(message: Message) {
|
||||
this.messages.push(message);
|
||||
ipcEmit("update-view", message);
|
||||
}
|
||||
|
||||
/* update an existing message */
|
||||
update(message: Message) {
|
||||
|
||||
/* get the target message */
|
||||
let target_message = this.messages.filter((m: Message) => {
|
||||
return m.uid = message.uid;
|
||||
})[0];
|
||||
|
||||
/* replace the target message */
|
||||
if (target_message) {
|
||||
target_message = message;
|
||||
ipcEmit("update-view", message);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
13
src/composables/useSaveToJSON.ts
Normal file
13
src/composables/useSaveToJSON.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
|
||||
import {config} from "@/config";
|
||||
import fs from 'fs';
|
||||
|
||||
export const saveToJson = (fileName: string, data: any) => {
|
||||
|
||||
fs.writeFile(config.configPath + fileName, JSON.stringify(data), (err) => {
|
||||
if (err) {
|
||||
console.log("Error when saving to json.")
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
89
src/composables/useWebsockets.ts
Normal file
89
src/composables/useWebsockets.ts
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
import WebSocket from 'ws';
|
||||
|
||||
|
||||
const _connectionCheckTimeout = 4000;
|
||||
const _reconnectTimeout = 1000;
|
||||
let _connectionCheckInterval: ReturnType<typeof setTimeout>;
|
||||
|
||||
|
||||
export default function useWebSockets(
|
||||
messageCallback: (message: string) => void,
|
||||
connectionStatusCallback: (alive: boolean) => void,
|
||||
) {
|
||||
|
||||
let socket: WebSocket;
|
||||
|
||||
const send = async (data: Record<string, any>): Promise<boolean> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (socket) {
|
||||
if (socket.readyState === WebSocket.OPEN) {
|
||||
socket.send(JSON.stringify(data));
|
||||
resolve(true);
|
||||
}
|
||||
}
|
||||
reject(false);
|
||||
});
|
||||
}
|
||||
|
||||
const connect = (socketUrl: string, secret: string) => {
|
||||
|
||||
// avoid setting multiple interval;
|
||||
if (_connectionCheckInterval) clearInterval(_connectionCheckInterval);
|
||||
|
||||
/* create a new socket */
|
||||
socket = new WebSocket(socketUrl);
|
||||
|
||||
/* add event listeners */
|
||||
socket.on("open", () => {
|
||||
|
||||
socket.send(JSON.stringify({key: secret}));
|
||||
|
||||
// ping server
|
||||
_connectionCheckInterval = setInterval(() => {
|
||||
|
||||
socket.ping(null, true, (e: Error) => {
|
||||
if (e) {
|
||||
socket.close();
|
||||
connectionStatusCallback(false);
|
||||
setTimeout(() => connect(socketUrl, secret), _reconnectTimeout);
|
||||
}
|
||||
});
|
||||
|
||||
}, _connectionCheckTimeout);
|
||||
|
||||
});
|
||||
|
||||
socket.on("message", (event: WebSocket.MessageEvent) => {
|
||||
console.log("message received", event);
|
||||
messageCallback(event.toString())
|
||||
});
|
||||
|
||||
socket.on("close", (event: WebSocket.CloseEvent) => {
|
||||
connectionStatusCallback(false);
|
||||
clearInterval(_connectionCheckInterval);
|
||||
if (!event.wasClean) {
|
||||
setTimeout(() => connect(socketUrl, secret), _reconnectTimeout);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
socket.on("pong", () => connectionStatusCallback(true));
|
||||
|
||||
}
|
||||
|
||||
const close = () => {
|
||||
if (socket) {
|
||||
socket.close();
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
connect,
|
||||
send,
|
||||
close
|
||||
};
|
||||
|
||||
}
|
||||
17
src/config.ts
Normal file
17
src/config.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
|
||||
import { app } from "electron";
|
||||
|
||||
const env = process.env;
|
||||
|
||||
const PLATFORM_PORT = env.PLATFORM_PORT || 8760;
|
||||
const PLATFORM_IP = env.PLATFORM_IP || 'http://127.0.0.1';
|
||||
|
||||
const BUSINESS_PORT = env.BUSINESS_PORT || 3000;
|
||||
const BUSINESS_IP = env.BUSINESS_IP || 'http://127.0.0.1';
|
||||
|
||||
export const config = {
|
||||
PLATFORM_URL: `${PLATFORM_IP}:${PLATFORM_PORT}`,
|
||||
BUSINESS_URL: `${BUSINESS_IP}:${BUSINESS_PORT}`,
|
||||
BUSINESS_PREFIX: '/api',
|
||||
configPath: app.getPath('userData')
|
||||
}
|
||||
52
src/init.ts
Normal file
52
src/init.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
/**
|
||||
* Entry point for Crimata electron app.
|
||||
* "Look on my Works, ye Mighty, and despair!"
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
import { app, protocol } from "electron";
|
||||
import createWindow from "./window";
|
||||
import main from "./main";
|
||||
import { backgroundMitt } from '@/composables/useEmitter';
|
||||
|
||||
console.log('Starting Crimata electron app.');
|
||||
|
||||
// Scheme must be registered before the app is ready
|
||||
protocol.registerSchemesAsPrivileged([
|
||||
{ scheme: "app", privileges: { secure: true, standard: true } }
|
||||
]);
|
||||
|
||||
const isDev = require('electron-is-dev');
|
||||
|
||||
let win: boolean;
|
||||
|
||||
// Listen for window creation.
|
||||
backgroundMitt.on('window-active', (state: boolean) => {
|
||||
win = state;
|
||||
});
|
||||
|
||||
/* Start main process on ready */
|
||||
app.on("ready", async () => {
|
||||
await main();
|
||||
});
|
||||
|
||||
// Must keep to ensure app doesn't quit on close.
|
||||
app.on("before-quit", async () => {
|
||||
});
|
||||
|
||||
// Must keep to ensure app doesn't quit on close.
|
||||
app.on("window-all-closed", () => {
|
||||
});
|
||||
|
||||
// When user clicks app icon (re-open)
|
||||
app.on("activate", () => {
|
||||
if (!win) createWindow();
|
||||
});
|
||||
|
||||
// Exit cleanly on request from parent process in development mode.
|
||||
if (isDev) {
|
||||
process.on("SIGTERM", () => {
|
||||
app.quit();
|
||||
});
|
||||
}
|
||||
28
src/ipc/handlers.ts
Normal file
28
src/ipc/handlers.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
import { accountLogin, accountLogout, accountProfile } from "@/account";
|
||||
import { IpcHandler } from "@/composables/useIpcMain";
|
||||
import { flush } from "@/audio";
|
||||
|
||||
|
||||
const LOGIN_CHANNEL = "invoke-account-login";
|
||||
const LOGOUT_CHANNEL = "invoke-account-logout";
|
||||
const GET_AUDIO_CHANNEL = "invoke-audio-flush";
|
||||
|
||||
export const loginHandler = new IpcHandler({
|
||||
channel: LOGIN_CHANNEL,
|
||||
handlerCallback: accountLogin
|
||||
});
|
||||
|
||||
export const logoutHandler = new IpcHandler({
|
||||
channel: LOGOUT_CHANNEL,
|
||||
handlerCallback: accountLogout
|
||||
});
|
||||
|
||||
export const getAudioHandler = new IpcHandler({
|
||||
channel: GET_AUDIO_CHANNEL,
|
||||
handlerCallback: flush
|
||||
});
|
||||
|
||||
|
||||
33
src/ipc/index.ts
Normal file
33
src/ipc/index.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
import * as handlers from "./handlers";
|
||||
import * as listeners from "./listeners";
|
||||
|
||||
const ipcHandlers: IPCHandlers = {};
|
||||
const ipcListeners: IPCListeners = {};
|
||||
|
||||
const _initHandlers = (): void => {
|
||||
for (const [key, handler] of Object.entries(handlers)) {
|
||||
if (!(key in ipcHandlers)) {
|
||||
ipcHandlers[key] = handler;
|
||||
handler.handle();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const _initListeners = (): void => {
|
||||
for (const [key, listener] of Object.entries(listeners)) {
|
||||
if (!(key in ipcListeners)) {
|
||||
ipcListeners[key] = listener;
|
||||
listener.listen();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default function initIpcMain(): void {
|
||||
_initHandlers();
|
||||
_initListeners();
|
||||
}
|
||||
|
||||
|
||||
24
src/ipc/listeners.ts
Normal file
24
src/ipc/listeners.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
|
||||
import { IpcListener } from "@/composables/useIpcMain"
|
||||
import { sendMessage } from '@/session';
|
||||
import { collect } from "@/audio";
|
||||
import { updateAppState } from "@/account";
|
||||
|
||||
const CLIENT_MESSAGE_CHANNEL = "post-session-send"
|
||||
const GET_AUDIO_CHANNEL = "post-audio-collect";
|
||||
const APP_MOUNT_CHANNEL = "post-app-mount";
|
||||
|
||||
export const messageListener = new IpcListener<Message>({
|
||||
channel: CLIENT_MESSAGE_CHANNEL,
|
||||
listenerCallback: sendMessage
|
||||
});
|
||||
|
||||
export const audioChunkListener = new IpcListener<ArrayBuffer>({
|
||||
channel: GET_AUDIO_CHANNEL,
|
||||
listenerCallback: collect
|
||||
});
|
||||
|
||||
export const appMountListener = new IpcListener<null>({
|
||||
channel: APP_MOUNT_CHANNEL,
|
||||
listenerCallback: updateAppState
|
||||
});
|
||||
44
src/main.ts
44
src/main.ts
|
|
@ -1,17 +1,39 @@
|
|||
// src/main.ts
|
||||
/**
|
||||
* Where the background logic really begins, gets called by app.onReady().
|
||||
*
|
||||
* Handles authentication. If profile is set, we launch a session, which consis
|
||||
* of opening a connection with the platform, initializing the audio streams.
|
||||
*
|
||||
* The session is primarily an interface between the frontend and the platform,
|
||||
* relaying messages from one to the other.
|
||||
*
|
||||
*/
|
||||
|
||||
import App from "./App.vue";
|
||||
import router from "./router";
|
||||
import initIpcMain from "@/ipc/index";
|
||||
import { accountAuth, updateAppState } from "./account";
|
||||
import { launchSession } from "./session";
|
||||
import createWindow from "./window";
|
||||
|
||||
import mitt from "mitt";
|
||||
import { createApp } from "vue";
|
||||
let authState: AuthState | null;
|
||||
|
||||
export default async function main() {
|
||||
|
||||
// Handle events.
|
||||
const emitter = mitt();
|
||||
/* initiate controls for frontend to use when needed */
|
||||
initIpcMain();
|
||||
|
||||
const app = createApp(App)
|
||||
/* launch browser window */
|
||||
await createWindow();
|
||||
|
||||
app.use(router)
|
||||
app.provide("mitt", emitter)
|
||||
app.mount("#app");
|
||||
try {
|
||||
authState = await accountAuth() as AuthState;
|
||||
} catch(e) {
|
||||
console.log('AUTH:', e);
|
||||
authState = null;
|
||||
} finally {
|
||||
if (authState) {
|
||||
launchSession(authState.token as string);
|
||||
}
|
||||
updateAppState();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,71 +0,0 @@
|
|||
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';
|
||||
|
||||
let token: string | null;
|
||||
|
||||
token = window.localStorage.getItem(AUTH_KEY);
|
||||
if (token) {
|
||||
state.accessToken = token;
|
||||
window.localStorage.setItem(AUTH_KEY, token);
|
||||
}
|
||||
|
||||
// Load key and invoke onAuthSession to try key-login.
|
||||
const authToken = async () => {
|
||||
console.log("Calling auth token.")
|
||||
const { invoke } = useIpc();
|
||||
try {
|
||||
token = window.localStorage.getItem(AUTH_KEY);
|
||||
|
||||
if (!token) {
|
||||
token = "no-token"
|
||||
}
|
||||
|
||||
console.log(`Token ${token}`)
|
||||
const res = await invoke('auth-session', token);
|
||||
window.localStorage.setItem(AUTH_KEY, res);
|
||||
state.accessToken = res;
|
||||
}
|
||||
catch(e) {
|
||||
state.error = e;
|
||||
console.log('ERROR: failed authentication');
|
||||
window.localStorage.removeItem(AUTH_KEY);
|
||||
state.accessToken = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Gets called on socket open.
|
||||
window.ipcRenderer.on("auth", async (_event, _arg) => {
|
||||
await authToken();
|
||||
});
|
||||
|
||||
export const useAuth = () => {
|
||||
|
||||
const setToken = (token: string) => {
|
||||
window.localStorage.setItem(AUTH_KEY, token);
|
||||
state.accessToken = token;
|
||||
state.error = undefined;
|
||||
}
|
||||
|
||||
const logout = (): Promise<null> => {
|
||||
window.localStorage.removeItem(AUTH_KEY);
|
||||
return Promise.resolve(state.accessToken = null);
|
||||
}
|
||||
|
||||
return {
|
||||
setToken,
|
||||
logout,
|
||||
...toRefs(state), // accessToken, error
|
||||
}
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
|
||||
export const useIpc = () => {
|
||||
const invoke = async (endpoint: string, payload: any) => {
|
||||
try {
|
||||
const res = await window.ipcRenderer.invoke(endpoint, payload);
|
||||
return res;
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
const post = (endpoint: string, payload: any) => {
|
||||
window.postMessage({
|
||||
endpoint: endpoint,
|
||||
content: payload
|
||||
}, '*')
|
||||
};
|
||||
|
||||
return {
|
||||
invoke,
|
||||
post
|
||||
}
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
import { RenderMessage, ClientMessage } from "@/types/message/index";
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
function getTimeStamp(): number {
|
||||
const currentdate = new Date();
|
||||
return currentdate.getTime();
|
||||
}
|
||||
|
||||
// Create a RenderMessage object.
|
||||
export const renderMessage = (text: boolean | string, audio: boolean | string, context: string, modifier: string): RenderMessage => (
|
||||
{
|
||||
content: {
|
||||
text: text,
|
||||
audio: audio
|
||||
},
|
||||
context: context,
|
||||
modifier: modifier,
|
||||
time: getTimeStamp(),
|
||||
uid: uuidv4(),
|
||||
isChild: "none"
|
||||
}
|
||||
)
|
||||
|
||||
export const clientMessage = (text: string, audio: string | boolean, uid: string): ClientMessage => (
|
||||
{
|
||||
audio,
|
||||
text,
|
||||
uid
|
||||
}
|
||||
)
|
||||
|
|
@ -1,103 +0,0 @@
|
|||
import { ref } from 'vue';
|
||||
import useScroll from "@/modules/scroll";
|
||||
import { RenderMessage, Annotation } from "@/types/message/index";
|
||||
|
||||
const messages = ref(new Map());
|
||||
|
||||
const addMessage = (message: RenderMessage) => {
|
||||
messages.value.set(message.uid, message)
|
||||
}
|
||||
|
||||
const updateMessage = (annotation: Annotation) => {
|
||||
const message = messages.value.get(annotation.uid)
|
||||
message.context = annotation.context
|
||||
message.content.text = annotation.text
|
||||
}
|
||||
|
||||
const loadMessages = () => {
|
||||
const rawData = window.localStorage.getItem("crimata_messages");
|
||||
if (rawData) {
|
||||
const messageData = JSON.parse(rawData)
|
||||
messages.value = new Map(Object.entries(messageData));
|
||||
}
|
||||
}
|
||||
|
||||
const saveMessages = () => {
|
||||
const messageData = Object.fromEntries(messages.value);
|
||||
window.localStorage.setItem("crimata_messages", JSON.stringify(messageData));
|
||||
}
|
||||
|
||||
const isSimmilar = (messageA: RenderMessage, messageB: RenderMessage) => {
|
||||
if ((Math.abs(messageA.time - messageB.time) < 20000) && (messageA.modifier == messageB.modifier) && (messageA.context == messageB.context)) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const updateGrouping = () => {
|
||||
console.log("updating grouping")
|
||||
const refs = Array.from(messages.value.keys())
|
||||
|
||||
// Get the last three messages.
|
||||
const first = messages.value.get(refs[refs.length - 1])
|
||||
const second = messages.value.get(refs[refs.length - 2])
|
||||
const third = messages.value.get(refs[refs.length - 3])
|
||||
|
||||
// If messages are simmilar, update the classes.
|
||||
if ((first) && (second)) {
|
||||
if (isSimmilar(first, second)) {
|
||||
first.isChild = "last" // i.e. last in group.
|
||||
second.isChild = "first"
|
||||
|
||||
if (third) {
|
||||
if ((third.isChild == "first") || (third.isChild == "middle")) {
|
||||
second.isChild = "middle"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { setScroll, updateScrollRef, adjustScroll } = useScroll("messenger");
|
||||
|
||||
loadMessages()
|
||||
setTimeout(setScroll, 1000);
|
||||
|
||||
|
||||
export default function useMessages() {
|
||||
|
||||
// Main function for updating the message view.
|
||||
const updateMessageView = (message: RenderMessage | Annotation) => {
|
||||
|
||||
// Step 1: See if user is scrolled down.
|
||||
updateScrollRef()
|
||||
|
||||
// Step 2: Add the new content to the view.
|
||||
if ("content" in message) {
|
||||
addMessage(message)
|
||||
} else {
|
||||
updateMessage(message)
|
||||
}
|
||||
|
||||
// Step 3: Pop off oldest message (if > 200).
|
||||
if (messages.value.size >= 200) {
|
||||
const oldest = Array.from(messages.value.keys()).shift();
|
||||
messages.value.delete(oldest);
|
||||
};
|
||||
|
||||
// Step 4: Update grouping.
|
||||
updateGrouping()
|
||||
|
||||
// Setp 5: Scroll the view (if scrolled down).
|
||||
setTimeout(adjustScroll, 20);
|
||||
|
||||
// Step 6: Save the view data.
|
||||
saveMessages()
|
||||
|
||||
}
|
||||
|
||||
return {
|
||||
messages,
|
||||
updateMessageView
|
||||
}
|
||||
};
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
|
||||
|
||||
export default function useScroll(element: string) {
|
||||
|
||||
let isScrolledToBottom: boolean;
|
||||
|
||||
// Set the initial scroll position.
|
||||
const setScroll = () => {
|
||||
const view = document.getElementById(element)
|
||||
|
||||
if (view) {
|
||||
view.scrollTo({
|
||||
top: view.scrollHeight - view.clientHeight,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Update isScrolledToBottom
|
||||
const updateScrollRef = () => {
|
||||
const view = document.getElementById(element)
|
||||
|
||||
if (view) {
|
||||
isScrolledToBottom = view.scrollHeight - view.clientHeight <= view.scrollTop + 1
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Adjust scroll after we add content to the messenger.
|
||||
const adjustScroll = () => {
|
||||
const view = document.getElementById(element)
|
||||
|
||||
if (view) {
|
||||
if (isScrolledToBottom) {
|
||||
view.scrollTo({
|
||||
top: view.scrollHeight - view.clientHeight,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return {
|
||||
updateScrollRef,
|
||||
adjustScroll,
|
||||
setScroll,
|
||||
}
|
||||
|
||||
}
|
||||
76
src/render/App.vue
Normal file
76
src/render/App.vue
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
<template>
|
||||
<!-- Only render when profile has been set -->
|
||||
<main id="app" v-if="authComplete">
|
||||
|
||||
<Header />
|
||||
|
||||
<Messenger
|
||||
v-if="profile"
|
||||
/>
|
||||
|
||||
<!-- Main Components
|
||||
-->
|
||||
<Login v-else />
|
||||
|
||||
</main>
|
||||
|
||||
<!-- Show spash screen if ready is false -->
|
||||
<Splash v-else />
|
||||
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
import { defineComponent, onMounted } from "vue";
|
||||
import { IpcRendererEvent } from "electron";
|
||||
|
||||
import Splash from "@/render/components/splash.vue";
|
||||
import Messenger from "@/render/components/messenger.vue";
|
||||
import Login from "@/render/components/login.vue";
|
||||
import Header from "@/render/components/header.vue";
|
||||
|
||||
import { profile, authComplete } from "@/render/composables/useProfile";
|
||||
import { postAppMount } from "@/render/ipc";
|
||||
|
||||
export default defineComponent({
|
||||
|
||||
components: {
|
||||
Splash,
|
||||
Messenger,
|
||||
Login,
|
||||
Header
|
||||
},
|
||||
|
||||
setup() {
|
||||
|
||||
onMounted(() => postAppMount());
|
||||
|
||||
return {
|
||||
authComplete,
|
||||
profile
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#app {
|
||||
font-family: "SF Pro Text";
|
||||
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
@ -1,111 +1,52 @@
|
|||
<template>
|
||||
<div
|
||||
class="message"
|
||||
:class="`${message.isChild}ChildMessage`"
|
||||
:id="`${message.modifier}Message`"
|
||||
:key="message.id"
|
||||
>
|
||||
|
||||
<!-- Three types of messages: self (sf), friend (fr), and crimata (ai) -->
|
||||
<!-- !denoted by the modifier attribute -->
|
||||
|
||||
<!-- Includes bubble and context. -->
|
||||
<div
|
||||
:id="`${message.modifier}MessageBox`"
|
||||
class="messageBox"
|
||||
>
|
||||
|
||||
<!-- message bubble -->
|
||||
<div
|
||||
class="bubble"
|
||||
:class="`${message.modifier}-${message.isChild}ChildBubble`"
|
||||
:id="`${message.modifier}Bubble`"
|
||||
>
|
||||
<!-- Show loader when audio is being transcribed. -->
|
||||
<div
|
||||
v-if="message.content.text === ''"
|
||||
class="contentLoader"
|
||||
>
|
||||
<div class="contentLoaderDot"></div>
|
||||
<div class="contentLoaderDot"></div>
|
||||
<div class="contentLoaderDot"></div>
|
||||
</div>
|
||||
|
||||
<!-- Show question mark if transcription comes back as unknown. -->
|
||||
<div
|
||||
v-else-if="message.content.text === false"
|
||||
class="questionMark"
|
||||
>
|
||||
?
|
||||
</div>
|
||||
|
||||
<!-- Else, show text. -->
|
||||
<div v-else>
|
||||
{{ message.content.text }}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- message context -->
|
||||
<span v-if="message.isChild === 'last' || message.isChild === 'none'" class="context">
|
||||
|
||||
<!-- Render Crimata icon or friend's initials. -->
|
||||
<div v-if="message.modifier == 'ai'" class="photo">
|
||||
<img src="@/assets/crimata.svg"/>
|
||||
</div>
|
||||
|
||||
<!-- Render Crimata icon or friend's initials. -->
|
||||
<div v-if="message.modifier == 'fr'" class="photo"
|
||||
:class="{ playing: isPlaying }"
|
||||
>
|
||||
{{ message.context[0] }}
|
||||
</div>
|
||||
|
||||
{{ message.context }}
|
||||
|
||||
</span>
|
||||
<div :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';
|
||||
|
||||
import { defineComponent, ref, onMounted } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: "MessageItem",
|
||||
name: "Bubble",
|
||||
|
||||
props: {
|
||||
message: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
props: ["text", "context", "type", "position"],
|
||||
|
||||
setup(props) {
|
||||
const isPlaying = ref(false);
|
||||
setup() {
|
||||
|
||||
const onStopPlayback = (e: any) => {
|
||||
window.ipcRenderer.removeAllListeners("stop-playback-anim");
|
||||
isPlaying.value = false
|
||||
}
|
||||
const seen = ref(false);
|
||||
/* initialize seen state */
|
||||
if (document.visibilityState === "visible") {
|
||||
seen.value = true;
|
||||
} else seen.value = false;
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
// Turn on audio playback animation if audio.
|
||||
if (props.message.content.audio === true) {
|
||||
window.ipcRenderer.on("stop-playback-anim", onStopPlayback);
|
||||
isPlaying.value = true
|
||||
}
|
||||
if(seen.value)
|
||||
document.addEventListener("visibilitychange", () => {
|
||||
seen.value = true
|
||||
});
|
||||
|
||||
})
|
||||
});
|
||||
|
||||
return {
|
||||
isPlaying
|
||||
}
|
||||
seen
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
})
|
||||
|
|
@ -116,13 +57,10 @@
|
|||
|
||||
.message {
|
||||
width: 100vw;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
padding-top: 9px;
|
||||
padding-bottom: 9px;
|
||||
|
||||
}
|
||||
|
||||
.message:first-child {
|
||||
|
|
@ -133,6 +71,38 @@
|
|||
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;
|
||||
}
|
||||
|
|
@ -159,6 +129,8 @@
|
|||
}
|
||||
|
||||
.messageBox {
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
|
|
@ -183,6 +155,8 @@
|
|||
}
|
||||
|
||||
.bubble {
|
||||
position: relative;
|
||||
|
||||
max-width: 66vw;
|
||||
|
||||
display: flex;
|
||||
|
|
@ -244,7 +218,33 @@
|
|||
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;
|
||||
|
||||
|
|
@ -311,4 +311,21 @@
|
|||
color: #357CA2;
|
||||
}
|
||||
|
||||
// .divider {
|
||||
// width: 100vw;
|
||||
// display: flex;
|
||||
// justify-content: center;
|
||||
// align-items: center;
|
||||
|
||||
// font-family: "SF Compact Display";
|
||||
// font-size: 12px;
|
||||
// font-weight: bold;
|
||||
// color: #9B9B9B;
|
||||
|
||||
// margin-bottom: 18px;
|
||||
// }
|
||||
|
||||
|
||||
</style>
|
||||
|
||||
|
||||
0
src/render/components/controllers/bubble.control.ts
Normal file
0
src/render/components/controllers/bubble.control.ts
Normal file
102
src/render/components/controllers/helpers.ts
Normal file
102
src/render/components/controllers/helpers.ts
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
import { ref } from "vue";
|
||||
import anime from "animejs";
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
|
||||
export function animateTextInput () {
|
||||
|
||||
const side = ref("right");
|
||||
|
||||
function show () {
|
||||
const t1 = (side.value === "right") ? 50 : -70;
|
||||
const t2 = (side.value === "right") ? 110 : -130;
|
||||
|
||||
anime({
|
||||
targets: '#textInput',
|
||||
opacity: [0, 1],
|
||||
translateX: [t1, t2],
|
||||
scale: [0.3, 1],
|
||||
duration: 500,
|
||||
easing: 'easeOutExpo',
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
function hide () {
|
||||
const t = (side.value === "right") ? 50 : -80;
|
||||
|
||||
anime({
|
||||
targets: '#textInput',
|
||||
opacity: [1, 0],
|
||||
translateX: t,
|
||||
scale: 0.3,
|
||||
duration: 500,
|
||||
easing: 'easeOutExpo',
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
function switchSide () {
|
||||
const t = (side.value === "right") ? -130 : 110;
|
||||
|
||||
anime({
|
||||
targets: '#textInput',
|
||||
translateX: t,
|
||||
duration: 500,
|
||||
easing: 'easeOutExpo',
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
return {
|
||||
side,
|
||||
show,
|
||||
hide,
|
||||
switchSide
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
export function animateAudioInput () {
|
||||
|
||||
function show () {
|
||||
anime({
|
||||
targets: '#recIcon',
|
||||
opacity: [0, 0.75],
|
||||
scale: [0.0, 1],
|
||||
duration: 250,
|
||||
easing: 'linear',
|
||||
})
|
||||
}
|
||||
|
||||
function hide () {
|
||||
anime({
|
||||
targets: '#recIcon',
|
||||
opacity: [0.75, 0],
|
||||
scale: [1, 0],
|
||||
duration: 250,
|
||||
easing: 'linear',
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
show,
|
||||
hide
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
|
||||
export function newMessage ({
|
||||
text=false,
|
||||
audio=false,
|
||||
context=false,
|
||||
uid=uuidv4()
|
||||
}) {
|
||||
return {
|
||||
text: text,
|
||||
audio: audio,
|
||||
context: context,
|
||||
uid: uid
|
||||
};
|
||||
}
|
||||
77
src/render/components/controllers/inputItem.control.audio.ts
Normal file
77
src/render/components/controllers/inputItem.control.audio.ts
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
import { onMounted, onUnmounted, ref, Ref } from "vue";
|
||||
import { postMessage } from "@/render/ipc";
|
||||
import { newMessage, animateAudioInput } from "./helpers";
|
||||
import { invokeReturnAudio, postAudioChunk } from "@/render/ipc";
|
||||
|
||||
export default function useAudioInputController (typing: Ref) {
|
||||
|
||||
const recording = ref(false);
|
||||
let mediaRecorder: MediaRecorder;
|
||||
|
||||
const { show, hide } = animateAudioInput();
|
||||
|
||||
// initialize audio
|
||||
const conf = {audio: true, video: false}
|
||||
navigator.mediaDevices.getUserMedia(conf).then((stream: MediaStream) => {
|
||||
|
||||
const options = {mimeType: 'audio/webm'};
|
||||
mediaRecorder = new MediaRecorder(stream, options);
|
||||
|
||||
// post any mew audio to backend
|
||||
mediaRecorder.addEventListener('dataavailable', (e: BlobEvent) => {
|
||||
e.data.arrayBuffer().then((buff: ArrayBuffer) => {
|
||||
postAudioChunk(buff);
|
||||
});
|
||||
});
|
||||
|
||||
// get audio and post new message to backend
|
||||
mediaRecorder.addEventListener('stop', (_e: Event) => {
|
||||
invokeReturnAudio().then((audio: ArrayBuffer[] | Error) => {
|
||||
console.log(audio);
|
||||
// postMessage(newMessage({audio: audio}));
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
// start recording on space bar
|
||||
const record = () => {
|
||||
console.log("INPT:Capturing audio...")
|
||||
mediaRecorder.start();
|
||||
recording.value = true;
|
||||
show()
|
||||
}
|
||||
|
||||
// stop recording and send on release
|
||||
const stop = () => {
|
||||
console.log("INPT:Stopping record.")
|
||||
mediaRecorder.stop();
|
||||
recording.value = false;
|
||||
hide();
|
||||
}
|
||||
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.keyCode == 32 && !typing.value) record();
|
||||
}
|
||||
|
||||
const onKeyUp = (e: KeyboardEvent) => {
|
||||
if (e.keyCode == 32 && recording.value) stop();
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener("keydown", onKeyDown);
|
||||
window.addEventListener("keyup", onKeyUp);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener("keydown", onKeyDown);
|
||||
window.removeEventListener("keyup", onKeyUp);
|
||||
})
|
||||
|
||||
return {
|
||||
recording
|
||||
}
|
||||
|
||||
}
|
||||
134
src/render/components/controllers/inputItem.control.text.ts
Normal file
134
src/render/components/controllers/inputItem.control.text.ts
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
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
|
||||
}
|
||||
|
||||
}
|
||||
38
src/render/components/controllers/messenger.control.ts
Normal file
38
src/render/components/controllers/messenger.control.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import { ref } from 'vue';
|
||||
import useScroll from "@/render/composables/useScroll";
|
||||
|
||||
const messagesRef = ref();
|
||||
|
||||
/* seed the canvas with messages */
|
||||
const seedCanvas = (messages: Message[]) => {
|
||||
messagesRef.value = messages;
|
||||
}
|
||||
|
||||
const addMessage = (message: Message) => {
|
||||
messagesRef.value.push(message);
|
||||
}
|
||||
|
||||
const updateMessage = (message: Message) => {
|
||||
|
||||
let target_message = messagesRef.value.filter((m: Message) => {
|
||||
return m.uid = message.uid;
|
||||
})[0];
|
||||
|
||||
if (target_message) {
|
||||
target_message = message;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default function useMessages() {
|
||||
|
||||
const { updateScrollRef, adjustScroll } = useScroll("messenger");
|
||||
|
||||
return {
|
||||
messagesRef,
|
||||
seedCanvas,
|
||||
addMessage,
|
||||
updateMessage
|
||||
};
|
||||
|
||||
}
|
||||
89
src/render/components/header.vue
Normal file
89
src/render/components/header.vue
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
<template>
|
||||
<button id="titlebar" />
|
||||
<span id="menu">
|
||||
<button
|
||||
class="menuButton exitButton"
|
||||
@click.prevent="postNavBarExit"
|
||||
/>
|
||||
<button
|
||||
class="menuButton minimizeButton"
|
||||
@click.prevent="postNavBarMin"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
// import { postNavBarExit, postNavBarMin } from "@/render/ipc";
|
||||
import { defineComponent } from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "Header",
|
||||
setup() {
|
||||
const postNavBarExit = () => {};
|
||||
const postNavBarMin = () => {};
|
||||
return {
|
||||
postNavBarExit,
|
||||
postNavBarMin
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
#titlebar {
|
||||
position: fixed;
|
||||
|
||||
width: 100vw;
|
||||
height: 54px;
|
||||
|
||||
opacity: 0.75;
|
||||
|
||||
background-color: #EBEBEB;
|
||||
|
||||
-webkit-app-region: drag;
|
||||
|
||||
border: none;
|
||||
outline: none;
|
||||
|
||||
z-index: 1;
|
||||
|
||||
}
|
||||
|
||||
#menu {
|
||||
position: fixed;
|
||||
margin-left: 20px;
|
||||
margin-top: 20px;
|
||||
|
||||
z-index: 2;
|
||||
|
||||
}
|
||||
|
||||
.menuButton {
|
||||
border: none;
|
||||
outline:none;
|
||||
min-width: 14px;
|
||||
min-height: 14px;
|
||||
border-radius: 7px;
|
||||
}
|
||||
|
||||
.exitButton {
|
||||
background-color: #FF6157;
|
||||
}
|
||||
|
||||
.exitButton:active {
|
||||
background: #c14645;
|
||||
}
|
||||
|
||||
.minimizeButton {
|
||||
background-color: #FFC12F;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.minimizeButton:active {
|
||||
background-color: #c08e38;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -13,7 +13,10 @@
|
|||
<span v-if="recording" class="pause"></span>
|
||||
|
||||
<!-- Show text input on key-down -->
|
||||
<TextInput />
|
||||
<input
|
||||
id="textInput"
|
||||
type="text"
|
||||
/>
|
||||
|
||||
<!-- Show suggestions menu on click -->
|
||||
<!-- <Menu /> -->
|
||||
|
|
@ -23,29 +26,22 @@
|
|||
|
||||
<script lang="ts">
|
||||
|
||||
import { defineComponent, ref, onMounted, onUnmounted } from "vue";
|
||||
import draggify from "@/modules/draggify";
|
||||
import { defineComponent } from "vue";
|
||||
import draggify from "@/render/composables/useDraggify";
|
||||
|
||||
|
||||
import TextInput from "@/components/inputItem/textInput.vue";
|
||||
import useTextInputController from
|
||||
"@/components/inputItem/controllers/textCtrl";
|
||||
"@/render/components/controllers/inputItem.control.text";
|
||||
import useAudioInputController from
|
||||
"@/components/inputItem/controllers/audioCtrl";
|
||||
"@/render/components/controllers/inputItem.control.audio";
|
||||
|
||||
export default defineComponent({
|
||||
name: "InputItem",
|
||||
components: {
|
||||
TextInput
|
||||
},
|
||||
|
||||
props: ["initials"],
|
||||
|
||||
setup() {
|
||||
|
||||
// Mark input item with user's initials.
|
||||
const initials = ref("")
|
||||
|
||||
// Set initials.
|
||||
const initialData = window.localStorage.getItem("initials")
|
||||
if (initialData) initials.value = initialData
|
||||
|
||||
// Default values for position.
|
||||
const xStart = 15;
|
||||
const yStart = window.innerHeight - 200;
|
||||
|
|
@ -57,25 +53,10 @@ export default defineComponent({
|
|||
const { typing } = useTextInputController(elementX)
|
||||
const { recording } = useAudioInputController(typing)
|
||||
|
||||
// Update profile functionality.
|
||||
const onUpdateProfile = (_event: any, payload: any) => {
|
||||
initials.value = payload.message.first[0] + payload.message.last[0]
|
||||
window.localStorage.setItem("initials", initials.value)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.ipcRenderer.on("update-profile", onUpdateProfile);
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.ipcRenderer.removeAllListeners("update-profile");
|
||||
})
|
||||
|
||||
return {
|
||||
elementX,
|
||||
elementY,
|
||||
recording,
|
||||
initials
|
||||
recording
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
@ -97,6 +78,9 @@ export default defineComponent({
|
|||
|
||||
z-index: 3;
|
||||
|
||||
// To prevent window drag when overlapping with titlebar.
|
||||
-webkit-app-region: no-drag;
|
||||
|
||||
// border: 4px solid #C6C6C6;
|
||||
border-radius: 50%;
|
||||
|
||||
|
|
@ -205,4 +189,32 @@ export default defineComponent({
|
|||
}
|
||||
}
|
||||
|
||||
#textInput {
|
||||
position: absolute;
|
||||
|
||||
opacity: 0;
|
||||
|
||||
min-width: 150px;
|
||||
height: 16px;
|
||||
|
||||
border-radius: 18px;
|
||||
|
||||
padding: 10px;
|
||||
margin-right: 10px;
|
||||
margin-left: 10px;
|
||||
|
||||
outline: none;
|
||||
border: none;
|
||||
pointer-events: none;
|
||||
|
||||
background-color: white;
|
||||
|
||||
z-index: -1;
|
||||
|
||||
box-shadow: 0px 0px 10px 0px rgba(0,0,0,0.15);
|
||||
|
||||
transform: translateX(50px) scale(0.3);
|
||||
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<form id="login" @submit.prevent="submit">
|
||||
<form id="login" @submit.prevent="submitForm">
|
||||
|
||||
|
||||
<!-- login title -->
|
||||
|
|
@ -11,14 +11,14 @@
|
|||
<div id="loginBox">
|
||||
<!-- username -->
|
||||
<div class="inputBox">
|
||||
<input class="input" type="text" v-model="email" />
|
||||
<input class="input" type="text" v-model="usr" />
|
||||
<div class="inputModifier">Email</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- password -->
|
||||
<div class="inputBox">
|
||||
<input class="input" type="password" v-model="password" />
|
||||
<input class="input" type="password" v-model="pwd" />
|
||||
<div class="inputModifier">Password</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -26,72 +26,55 @@
|
|||
<!-- submit button; position: fixed -->
|
||||
<button class="submitButton button" type="submit">Submit</button>
|
||||
|
||||
<div class="invalid" v-if="invalid">
|
||||
Incorrect Credentials
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
import { defineComponent, ref } from "vue";
|
||||
import { useAuth } from "@/modules/auth";
|
||||
import { useIpc } from "@/modules/ipc";
|
||||
import { useRouter } from "vue-router";
|
||||
import { setProfile } from '@/render/composables/useProfile';
|
||||
import { invokeLogin } from "@/render/ipc";
|
||||
|
||||
export default defineComponent({
|
||||
name: "Login",
|
||||
|
||||
setup() {
|
||||
|
||||
const { setToken } = useAuth();
|
||||
const router = useRouter();
|
||||
const usr = ref("");
|
||||
const pwd = ref("");
|
||||
|
||||
const email = ref("");
|
||||
const password = ref("");
|
||||
const invalid = ref(false);
|
||||
// Submit login credentials to the backend.
|
||||
const submitForm = async () => {
|
||||
|
||||
const { invoke } = useIpc();
|
||||
try {
|
||||
|
||||
const submit = async () => {
|
||||
const profile = await invokeLogin({
|
||||
email: usr.value,
|
||||
password: pwd.value
|
||||
}) as Profile;
|
||||
|
||||
const payload = {
|
||||
request: "login",
|
||||
email: email.value,
|
||||
password: password.value
|
||||
};
|
||||
setProfile(profile)
|
||||
|
||||
try {
|
||||
const res = await invoke('auth-session', payload);
|
||||
setToken(res);
|
||||
invalid.value = false;
|
||||
router.push({ name: "home" });
|
||||
}
|
||||
|
||||
catch(e) {
|
||||
invalid.value = true;
|
||||
console.log('Error loging in.');
|
||||
}
|
||||
} catch(e) {
|
||||
console.log(e);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
const switchView = () => {
|
||||
router.push({ name: "register" });
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
submit,
|
||||
email,
|
||||
password,
|
||||
switchView,
|
||||
invalid
|
||||
usr,
|
||||
pwd,
|
||||
submitForm
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
82
src/render/components/messenger.vue
Normal file
82
src/render/components/messenger.vue
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
<template>
|
||||
|
||||
<!-- Position fixed items -->
|
||||
<InputItem :initials="profile.initials"/>
|
||||
<Settings />
|
||||
<div id="recIcon" />
|
||||
|
||||
<!-- List of message bubbles. -->
|
||||
<div id="messenger">
|
||||
<Bubble
|
||||
v-for="message in messages"
|
||||
:text="message.content.text"
|
||||
:context="message.context"
|
||||
:key="message[0]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
import { defineComponent, onMounted, onUnmounted } from "vue";
|
||||
import InputItem from "@/render/components/inputItem.vue";
|
||||
import Settings from "@/render/components/settings.vue";
|
||||
import Bubble from "@/render/components/bubble.vue";
|
||||
|
||||
import { profile } from "@/render/composables/useProfile";
|
||||
import { messages } from "@/render/composables/useMessages";
|
||||
|
||||
export default defineComponent({
|
||||
name: "Messenger",
|
||||
|
||||
components: {
|
||||
InputItem,
|
||||
Settings,
|
||||
Bubble
|
||||
},
|
||||
|
||||
setup() {
|
||||
return {
|
||||
messages,
|
||||
profile
|
||||
};
|
||||
|
||||
},
|
||||
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
#messenger {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#messenger::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#recIcon {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
|
||||
opacity: 0;
|
||||
transform: scale(0.0);
|
||||
|
||||
margin-top: 24px;
|
||||
margin-right: 66px;
|
||||
|
||||
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background-color: #FF3B3B;
|
||||
|
||||
z-index: 2;
|
||||
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
@ -13,23 +13,23 @@
|
|||
|
||||
<!-- Settings div. -->
|
||||
<div id="settings" v-show="toggleSettings">
|
||||
</div>
|
||||
|
||||
<button
|
||||
v-if="toggleSettings"
|
||||
class="settingsOption"
|
||||
@click="onLogout"
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
import { defineComponent, ref } from "vue";
|
||||
import { useAuth } from "@/modules/auth";
|
||||
import { useIpc } from "@/modules/ipc";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
import { clearProfile } from "@/render/composables/useProfile"
|
||||
import { invokeLogout } from "@/render/ipc";
|
||||
|
||||
export default defineComponent({
|
||||
name: "Settings",
|
||||
|
|
@ -37,10 +37,6 @@
|
|||
setup() {
|
||||
const toggleSettings = ref(false);
|
||||
|
||||
const { post } = useIpc();
|
||||
const { logout } = useAuth();
|
||||
const router = useRouter();
|
||||
|
||||
// Listen for escape key to close settings.
|
||||
const onEscape = (e: any) => {
|
||||
if(e.key === "Escape") {
|
||||
|
|
@ -55,11 +51,18 @@
|
|||
window.addEventListener("keydown", onEscape);
|
||||
}
|
||||
|
||||
// When user clicks logout button in settings.
|
||||
const onLogout = () => {
|
||||
logout();
|
||||
router.push({ name: "login" });
|
||||
post("logout", "")
|
||||
// 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 {
|
||||
|
|
@ -146,6 +149,11 @@
|
|||
border: none;
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.settingsOption:hover {
|
||||
54
src/render/composables/useIpcRend.ts
Normal file
54
src/render/composables/useIpcRend.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
|
||||
import { IpcRendererEvent } from "electron";
|
||||
|
||||
export class IpcRendererListener<InputParam> implements IIpcListener<InputParam> {
|
||||
|
||||
readonly channel: string;
|
||||
|
||||
readonly _listenerCallback: IpcListenerCallback<InputParam>;
|
||||
|
||||
constructor(options: {
|
||||
channel: string;
|
||||
listenerCallback: IpcListenerCallback<InputParam>;
|
||||
}) {
|
||||
this.channel = options.channel;
|
||||
this._listenerCallback = options.listenerCallback;
|
||||
}
|
||||
|
||||
listen() {
|
||||
this.remove();
|
||||
window.ipcRenderer.on(this.channel, this._onPost);
|
||||
}
|
||||
|
||||
remove() {
|
||||
window.ipcRenderer.removeAllListeners(this.channel);
|
||||
}
|
||||
|
||||
private _onPost = (_e: IpcRendererEvent, payload: InputParam): void => {
|
||||
console.log(`[IPC] Post: ${this.channel}`);
|
||||
this._listenerCallback(payload);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
export default function useIpcRenderer () {
|
||||
|
||||
const invoke = async (endpoint: string, payload: any) => {
|
||||
try {
|
||||
const res = await window.ipcRenderer.invoke(endpoint, payload);
|
||||
return res;
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
const post = (endpoint: string, payload: any) => {
|
||||
window.ipcRenderer.send(endpoint, payload);
|
||||
};
|
||||
|
||||
return {
|
||||
invoke,
|
||||
post,
|
||||
}
|
||||
}
|
||||
33
src/render/composables/useMessages.ts
Normal file
33
src/render/composables/useMessages.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
// shared
|
||||
import { ref, Ref } from "vue";
|
||||
import useScroll from "@/render/composables/useScroll";
|
||||
|
||||
export const messages: Ref<Array<Message>> = ref([]);
|
||||
|
||||
export const setMessages: IpcListenerCallback<Array<Message>> = (payload) => {
|
||||
messages.value = payload as Array<Message>;
|
||||
}
|
||||
|
||||
export const addMessage: IpcListenerCallback<Message> = (payload) => {
|
||||
messages.value.push(payload as Message);
|
||||
}
|
||||
|
||||
export const updateMessage: IpcListenerCallback<Message> = (payload) => {
|
||||
const message = payload as Message;
|
||||
|
||||
let targetMessage = messages.value.filter((m: Message) => {
|
||||
return m.uid = message.uid;
|
||||
})[0];
|
||||
|
||||
if (targetMessage) {
|
||||
targetMessage = message;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default {
|
||||
messages,
|
||||
setMessages,
|
||||
addMessage,
|
||||
updateMessage
|
||||
};
|
||||
27
src/render/composables/useProfile.ts
Normal file
27
src/render/composables/useProfile.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
// shared
|
||||
import { ref } from "vue";
|
||||
|
||||
export const profile = ref();
|
||||
|
||||
export const authComplete = ref(false);
|
||||
|
||||
export const setProfile: IpcListenerCallback<Profile | null> = (payload) => {
|
||||
payload ? profile.value = payload : clearProfile();
|
||||
showRender();
|
||||
};
|
||||
|
||||
export const clearProfile = () => {
|
||||
profile.value = null;
|
||||
};
|
||||
|
||||
export const showRender = () => {
|
||||
authComplete.value = true;
|
||||
};
|
||||
|
||||
export default {
|
||||
setProfile,
|
||||
clearProfile,
|
||||
profile,
|
||||
showRender,
|
||||
authComplete,
|
||||
};
|
||||
29
src/render/composables/useScroll.ts
Normal file
29
src/render/composables/useScroll.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
|
||||
|
||||
export default function useScroll(element: string) {
|
||||
|
||||
let isScrolledToBottom: boolean;
|
||||
const view = document.getElementById(element)
|
||||
|
||||
// Update isScrolledToBottom
|
||||
const updateScrollRef = () => {
|
||||
if (view) isScrolledToBottom = view.scrollHeight - view.clientHeight <= view.scrollTop + 1;
|
||||
return isScrolledToBottom;
|
||||
}
|
||||
|
||||
// Adjust scroll after we add content to the messenger.
|
||||
const adjustScroll = () => {
|
||||
if (view) {
|
||||
view.scrollTo({
|
||||
top: view.scrollHeight - view.clientHeight,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
updateScrollRef,
|
||||
adjustScroll,
|
||||
};
|
||||
|
||||
}
|
||||
80
src/render/ipc.ts
Normal file
80
src/render/ipc.ts
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
|
||||
import useIpc from "@/render/composables/useIpcRend";
|
||||
import * as rendererListeners from "./listeners";
|
||||
|
||||
const { post, invoke } = useIpc();
|
||||
|
||||
/**
|
||||
*
|
||||
* Account and auth related endpoints
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
export const invokeLogin = async (
|
||||
payload: LoginPayload
|
||||
): Promise<Profile | Error> => (
|
||||
await invoke('invoke-account-login', JSON.stringify(payload))
|
||||
);
|
||||
|
||||
export const invokeLogout = async (): Promise<void> => (
|
||||
await invoke("invoke-account-logout", null)
|
||||
);
|
||||
|
||||
/**
|
||||
*
|
||||
* Audio endpoints
|
||||
*
|
||||
*/
|
||||
|
||||
export const postAudioChunk = (chunk: ArrayBuffer): void => (
|
||||
post("post-audio-collect", chunk)
|
||||
);
|
||||
|
||||
export const invokeReturnAudio = async (): Promise<ArrayBuffer[] | Error> => (
|
||||
await invoke("invoke-audio-flush", null)
|
||||
);
|
||||
|
||||
/**
|
||||
*
|
||||
* Crimata Platform (session) endpoints
|
||||
*
|
||||
*/
|
||||
|
||||
export const invokeSession = async (cid: string): Promise<Profile | Error> => (
|
||||
await invoke("messenger-init", cid)
|
||||
);
|
||||
|
||||
export const postMessage = (payload: Message): void => (
|
||||
post('post-session-send', payload)
|
||||
);
|
||||
|
||||
export const postAppMount = (): void => (
|
||||
post('post-app-mount', null)
|
||||
);
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* Ipc Renderer Listeners
|
||||
*
|
||||
*/
|
||||
|
||||
let ipcListeners: IPCListeners = {};
|
||||
|
||||
export const initIpcRendererListeners = () => {
|
||||
for (const [key, listener] of Object.entries(rendererListeners)) {
|
||||
if (!(key in ipcListeners)) {
|
||||
ipcListeners[key] = listener;
|
||||
listener.listen();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const removeListeners = () => {
|
||||
for (const [key, listener] of Object.entries(rendererListeners)) {
|
||||
listener.remove();
|
||||
}
|
||||
ipcListeners = {};
|
||||
};
|
||||
|
||||
32
src/render/listeners.ts
Normal file
32
src/render/listeners.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
|
||||
import { IpcRendererListener } from "./composables/useIpcRend"
|
||||
import { setProfile } from "./composables/useProfile";
|
||||
import { setMessages, addMessage, updateMessage } from "./composables/useMessages";
|
||||
|
||||
const SET_PROFILE_CHANNEL = "set-profile";
|
||||
|
||||
const INIT_MESSAGES_CHANNEL = "init-messages";
|
||||
const ADD_MESSAGE_CHANNEL = "add-message";
|
||||
const UPDATE_MESSAGE_CHANNEL = "update-message";
|
||||
|
||||
export const setProfileListener = new IpcRendererListener({
|
||||
channel: SET_PROFILE_CHANNEL,
|
||||
listenerCallback: setProfile
|
||||
});
|
||||
|
||||
export const initMessagesListener = new IpcRendererListener({
|
||||
channel: INIT_MESSAGES_CHANNEL,
|
||||
listenerCallback: setMessages
|
||||
});
|
||||
|
||||
|
||||
export const addMessagesListener = new IpcRendererListener({
|
||||
channel: ADD_MESSAGE_CHANNEL,
|
||||
listenerCallback: addMessage
|
||||
});
|
||||
|
||||
export const updateMessagesListener = new IpcRendererListener({
|
||||
channel: UPDATE_MESSAGE_CHANNEL,
|
||||
listenerCallback: updateMessage
|
||||
});
|
||||
|
||||
21
src/render/main.ts
Normal file
21
src/render/main.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
|
||||
// src/main.ts
|
||||
|
||||
import App from "./App.vue";
|
||||
|
||||
import mitt from "mitt";
|
||||
import { createApp } from "vue";
|
||||
|
||||
import { initIpcRendererListeners } from "./ipc"
|
||||
|
||||
|
||||
// Handle ipcMain events.
|
||||
initIpcRendererListeners();
|
||||
|
||||
// Handle events.
|
||||
const emitter = mitt();
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
app.provide("mitt", emitter);
|
||||
app.mount("#app");
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
import {
|
||||
createRouter,
|
||||
createWebHistory,
|
||||
createWebHashHistory,
|
||||
RouteRecordRaw
|
||||
} from "vue-router";
|
||||
import Home from "@/views/messenger.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: "/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
Normal file
101
src/session.ts
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
|
||||
|
||||
|
||||
|
||||
// import useAudio from "@/audio";
|
||||
import { ipcEmit } from "@/composables/useEmitter";
|
||||
import useWebsockets from "./composables/useWebsockets";
|
||||
import {config} from "@/config";
|
||||
|
||||
/* data structure of messages that's tied to the UI */
|
||||
const uiState: any | null = null;
|
||||
|
||||
/* start and stop audio functionality */
|
||||
// const { initAudio, closeAudio } = useAudio();
|
||||
|
||||
const isInitMessage = (message: any): boolean => {
|
||||
return true;
|
||||
};
|
||||
|
||||
const deauthenticate = (): void => {
|
||||
console.log('deauthenticating')
|
||||
};
|
||||
|
||||
const isAddMessage = (message: any): boolean => {
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Controls for interfacing with the platform.
|
||||
* Takes an onMessage callback which we define below.
|
||||
*/
|
||||
|
||||
|
||||
const onMessageCallback = (payload: string) => {
|
||||
|
||||
const message = JSON.parse(payload);
|
||||
console.log(typeof message);
|
||||
|
||||
/* if the platform fails to authenticate, we must back down */
|
||||
if (message === "CLOSE_AUTH_FAIL") {
|
||||
deauthenticate();
|
||||
return;
|
||||
}
|
||||
|
||||
ipcEmit("add-message", message)
|
||||
return
|
||||
|
||||
/* on init, platform sends state, used to init canvas */
|
||||
if (isInitMessage(message)) {
|
||||
ipcEmit("init-messages", message)
|
||||
}
|
||||
|
||||
|
||||
else if (isAddMessage(message)) {
|
||||
ipcEmit("add-messages", message)
|
||||
}
|
||||
|
||||
else {
|
||||
ipcEmit("update-messages", message)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const onConnectionStatusCallback = (alive: boolean) => {
|
||||
// console.log('[Session]: Connection Alive: ', alive);
|
||||
// ipcEmit('connection-state', alive);
|
||||
}
|
||||
|
||||
const { connect, send, close } = useWebsockets(
|
||||
onMessageCallback,
|
||||
onConnectionStatusCallback
|
||||
);
|
||||
|
||||
/* send a message to the platform */
|
||||
export function sendMessage<Message>(message: Message): void {
|
||||
|
||||
/* socket send */
|
||||
send(message);
|
||||
|
||||
}
|
||||
|
||||
|
||||
/* launch a new session (the main process for authenticated users) */
|
||||
export function launchSession(platformKey: string) {
|
||||
|
||||
/* connect to the platform */
|
||||
connect(config.PLATFORM_URL, platformKey);
|
||||
|
||||
/* initialize the audio streams */
|
||||
// initAudio();
|
||||
|
||||
}
|
||||
|
||||
|
||||
export function endSession() {
|
||||
|
||||
// closeAudio();
|
||||
|
||||
close();
|
||||
|
||||
}
|
||||
31
src/store.ts
Normal file
31
src/store.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
const Store = require('electron-store');
|
||||
|
||||
const schema = {
|
||||
token: {
|
||||
type: 'string',
|
||||
},
|
||||
profile: {}
|
||||
};
|
||||
|
||||
const store = new Store({
|
||||
schema,
|
||||
encryptionKey: "super user test"
|
||||
});
|
||||
|
||||
export const getToken = (): string | undefined => (store.get("token"));
|
||||
|
||||
export const clearToken = (): void => (store.delete("token"));
|
||||
|
||||
export const setToken = (token: string): void => (store.set('token', token));
|
||||
|
||||
export const setProfile = (profile: Profile): Profile => (store.set('profile', profile));
|
||||
|
||||
export const getProfile = (): Profile => (store.get('profile'));
|
||||
|
||||
export const clearProfile = (): void => (store.delete('profile'));
|
||||
|
||||
export const clearStore = (): void => {
|
||||
clearToken();
|
||||
clearProfile();
|
||||
}
|
||||
|
||||
77
src/types.ts
Normal file
77
src/types.ts
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
export interface RenderMessage {
|
||||
content: {
|
||||
text: boolean | string;
|
||||
audio: boolean | string;
|
||||
};
|
||||
context: string;
|
||||
modifier: string;
|
||||
time: number;
|
||||
uid: string;
|
||||
isChild: string;
|
||||
}
|
||||
|
||||
export interface ClientMessage {
|
||||
text: string;
|
||||
audio: boolean | string;
|
||||
uid: string;
|
||||
}
|
||||
|
||||
export interface Creds {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
// Possible messages from server:
|
||||
|
||||
export interface Profile {
|
||||
crimata_id: string;
|
||||
title: string;
|
||||
first: string;
|
||||
middle: string;
|
||||
last: string;
|
||||
suffix: string | number;
|
||||
nickname: string;
|
||||
full: string;
|
||||
}
|
||||
|
||||
export interface Annotation {
|
||||
text: string;
|
||||
context: string;
|
||||
uid: string;
|
||||
}
|
||||
|
||||
export interface StandardMessage {
|
||||
content: {
|
||||
text: boolean | string;
|
||||
audio: boolean | string;
|
||||
};
|
||||
context: string;
|
||||
modifier: string;
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
import { Emitter } from "mitt";
|
||||
|
||||
export type Mitt = Emitter;
|
||||
|
|
@ -1,97 +0,0 @@
|
|||
<template>
|
||||
<InputItem />
|
||||
<Settings />
|
||||
|
||||
<div id="recIcon" />
|
||||
|
||||
<!-- List of message bubbles. -->
|
||||
<div id="messenger">
|
||||
<MessageItem v-for="message in messages" :message="message[1]" :key="message[0]" />
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, onMounted, onUnmounted } from "vue";
|
||||
import MessageItem from "@/components/messageItem.vue";
|
||||
import InputItem from "@/components/inputItem/inputItem.vue";
|
||||
import Settings from "@/components/settings.vue";
|
||||
import useMitt from "@/modules/mitt";
|
||||
import useMessages from '@/modules/messages';
|
||||
|
||||
export default defineComponent({
|
||||
name: "Messenger",
|
||||
components: {
|
||||
MessageItem,
|
||||
InputItem,
|
||||
Settings
|
||||
},
|
||||
setup() {
|
||||
|
||||
// Listen for self-messages from inputItem.
|
||||
const { emitter } = useMitt();
|
||||
|
||||
// Handle messages in view.
|
||||
const { messages, updateMessageView } = useMessages();
|
||||
|
||||
// Update message view on new content.
|
||||
const onNewContent = (_event: any, payload: any) => {
|
||||
updateMessageView(payload.message)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
// Front end listener.
|
||||
emitter.on('self-message', (message) => updateMessageView(message));
|
||||
|
||||
// Back end listener.
|
||||
window.ipcRenderer.on("render-message", onNewContent);
|
||||
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.ipcRenderer.removeAllListeners("render-message");
|
||||
window.ipcRenderer.removeAllListeners("annotate-message");
|
||||
emitter.all.clear();
|
||||
});
|
||||
|
||||
return {
|
||||
messages
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
#messenger {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#messenger::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#recIcon {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
|
||||
opacity: 0;
|
||||
transform: scale(0.0);
|
||||
|
||||
margin-top: 24px;
|
||||
margin-right: 66px;
|
||||
|
||||
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background-color: #FF3B3B;
|
||||
|
||||
z-index: 2;
|
||||
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
|
|
@ -1,197 +0,0 @@
|
|||
<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>
|
||||
|
||||
<div v-if="invalidEmail" class="invalid">Invalid Email.</div>
|
||||
<div v-else-if="weakPass" class="invalid">Password must be 8-15 characters long.
|
||||
<br>Password must have at least one:
|
||||
<br>  Upper case letter
|
||||
<br>  Lower case letter
|
||||
<br>  One numeric digit
|
||||
<br>  One special character
|
||||
</div>
|
||||
<div v-else-if="passUnMatch" class="invalid">Passwords dont match.</div>
|
||||
|
||||
</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 router = useRouter();
|
||||
const { invoke } = useIpc();
|
||||
const { setToken } = useAuth();
|
||||
|
||||
const email = ref("");
|
||||
const password = ref("");
|
||||
const password2 = ref("");
|
||||
const invalidEmail = ref(false);
|
||||
const weakPass = ref(false);
|
||||
const passUnMatch = ref(false);
|
||||
|
||||
// emai must be in '@email.com' format
|
||||
const emailReg = /^(([^<>()\[\]\.,;:\s@\"]+(\.[^<>()\[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i;
|
||||
// password must be 8-15 chars, have one upper, lower, number, and special char
|
||||
const pwReg = /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[^a-zA-Z0-9])(?!.*\s).{8,15}$/;
|
||||
|
||||
// call this function on form submit.
|
||||
const submit = async () => {
|
||||
// reset invalid credentials references.
|
||||
invalidEmail.value = false;
|
||||
weakPass.value = false;
|
||||
passUnMatch.value = false;
|
||||
|
||||
// validate email.
|
||||
if (!emailReg.test(email.value)) {
|
||||
invalidEmail.value = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// validate password strength.
|
||||
if (!pwReg.test(password.value)) {
|
||||
weakPass.value = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// check password match
|
||||
if (password.value !== password2.value) {
|
||||
passUnMatch.value = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const payload: RegisterPayload = {
|
||||
request: "register",
|
||||
email: email.value,
|
||||
password: password.value,
|
||||
};
|
||||
|
||||
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,
|
||||
invalidEmail,
|
||||
weakPass,
|
||||
passUnMatch,
|
||||
};
|
||||
},
|
||||
});
|
||||
</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;
|
||||
}
|
||||
|
||||
.invalid {
|
||||
font-family: "SF Compact Display";
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
color: #F53737;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,22 +1,39 @@
|
|||
"use strict";
|
||||
|
||||
import { BrowserWindow, ipcMain } from "electron";
|
||||
import { BrowserWindow, ipcMain, app } from "electron";
|
||||
import { createProtocol } from "vue-cli-plugin-electron-builder/lib";
|
||||
import { backgroundMitt } from './emitter';
|
||||
import { RenderMessage } from "@/types/message/index";
|
||||
import { backgroundMitt } from './composables/useEmitter';
|
||||
import { saveToJson } from "./composables/useSaveToJSON";
|
||||
import * as path from "path";
|
||||
import fs from 'fs';
|
||||
|
||||
interface IpcRendererPayload {
|
||||
endpoint: string;
|
||||
message: RenderMessage | null;
|
||||
}
|
||||
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 => {
|
||||
console.log("onNavBar")
|
||||
if (win) {
|
||||
if (action === "close") {
|
||||
win.close()
|
||||
|
|
@ -27,46 +44,45 @@ const onNavBar = (_event: any, action: string): void => {
|
|||
}
|
||||
|
||||
// Util function to render message on ipc-renderer event.
|
||||
const renderMessage = (payload: IpcRendererPayload): void => {
|
||||
const postToWindow = <T>(event: IpcRendererEvent<T>): void => {
|
||||
if (win) {
|
||||
win.webContents.send(payload.endpoint, {
|
||||
message: payload.message
|
||||
});
|
||||
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 (win) {
|
||||
const bounds = win.getBounds();
|
||||
const position = win.getPosition();
|
||||
|
||||
const state = JSON.stringify(
|
||||
{
|
||||
w: bounds.width,
|
||||
h: bounds.height,
|
||||
x: position[0],
|
||||
y: position[1]
|
||||
}
|
||||
);
|
||||
if (winState) {
|
||||
|
||||
winState.width = bounds.width;
|
||||
winState.height = bounds.height;
|
||||
winState.x = position[0];
|
||||
winState.y = position[1]
|
||||
|
||||
saveToJson("window.json", winState)
|
||||
|
||||
fs.writeFile('windowState.json', state, (err) => {
|
||||
if (err) throw err;
|
||||
return;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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.removeHandler('nav-bar'); // avoid setting duplicate handlers
|
||||
ipcMain.handle('nav-bar', onNavBar);
|
||||
ipcMain.removeAllListeners("nav-bar") // avoid setting duplicate handlers
|
||||
ipcMain.on("nav-bar", onNavBar);
|
||||
|
||||
// Gateway for messages to the frontend.
|
||||
backgroundMitt.on('ipc-renderer', renderMessage);
|
||||
backgroundMitt.removeAllListeners("ipc-renderer")
|
||||
backgroundMitt.on("ipc-renderer", postToWindow);
|
||||
|
||||
}
|
||||
|
||||
// Do this on window dismount (close).
|
||||
|
|
@ -76,21 +92,21 @@ const onWindowDismount = (): void => {
|
|||
}
|
||||
|
||||
// function used by run.ts to create the main window.
|
||||
export async function createWindow(): Promise<void> {
|
||||
export default async function createWindow(): Promise<void> {
|
||||
return new Promise((resolve, _reject) => {
|
||||
|
||||
// avoid creating duplicate windows.
|
||||
if (win) resolve();
|
||||
|
||||
// Load the saved window state.
|
||||
const state = JSON.parse(fs.readFileSync('windowState.json').toString());
|
||||
winState = loadWinState("window.json")
|
||||
|
||||
// Define the browser window.
|
||||
win = new BrowserWindow({
|
||||
width: state.w,
|
||||
height: state.h,
|
||||
x: state.x,
|
||||
y: state.y,
|
||||
width: winState.width,
|
||||
height: winState.height,
|
||||
x: winState.x as number,
|
||||
y: winState.y as number,
|
||||
resizable: true,
|
||||
backgroundColor: '#EBEBEB',
|
||||
frame: false,
|
||||
|
|
@ -119,9 +135,11 @@ export async function createWindow(): Promise<void> {
|
|||
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
|
||||
|
|
@ -133,3 +151,11 @@ export async function createWindow(): Promise<void> {
|
|||
|
||||
});
|
||||
}
|
||||
|
||||
autoUpdater.on('update-available', () => {
|
||||
if (win) win.webContents.send('update_available');
|
||||
});
|
||||
|
||||
autoUpdater.on('update-downloaded', () => {
|
||||
if (win) win.webContents.send('update_downloaded');
|
||||
});
|
||||
|
|
@ -86,7 +86,7 @@ function testCallback() {
|
|||
}
|
||||
|
||||
function splitArrayIntoChunksOfLen(buf, len) {
|
||||
let chunks = [];
|
||||
const chunks = [];
|
||||
let i = 0;
|
||||
let L = len;
|
||||
console.log(buf.byteLength)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ const wss = new WebSocket.Server({
|
|||
let auth = false;
|
||||
|
||||
wss.on("connection", function connection(ws, req) {
|
||||
|
||||
ws.on("message", function incoming(message) {
|
||||
|
||||
console.log(message)
|
||||
|
|
@ -19,13 +20,13 @@ wss.on("connection", function connection(ws, req) {
|
|||
auth = true;
|
||||
} else {
|
||||
const parsed = JSON.parse(message);
|
||||
console.log('got a message', message)
|
||||
console.log('got a message', message);
|
||||
|
||||
}
|
||||
|
||||
} else {
|
||||
const parsed = JSON.parse(message);
|
||||
console.log('parsed', parsed)
|
||||
console.log('parsed', parsed);
|
||||
}
|
||||
|
||||
});
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ test('Window Loads Properly', async () => {
|
|||
// App is loaded properly
|
||||
expect(
|
||||
/Welcome to Your Vue\.js (\+ TypeScript )?App/.test(
|
||||
await client.getHTML('#app')
|
||||
await (await app.client.$('#app')).getHTML()
|
||||
)
|
||||
).toBe(true)
|
||||
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@
|
|||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"src/*.ts",
|
||||
"src/**/*.ts",
|
||||
"src/**/*.tsx",
|
||||
|
|
|
|||
|
|
@ -1,29 +0,0 @@
|
|||
// NOTE needed imports for rust wasm
|
||||
// const path = require("path");
|
||||
// const WasmPackPlugin = require("@wasm-tool/wasm-pack-plugin");
|
||||
// const OptimizeWasmPlugin = require("optimize-wasm-webpack-plugin");
|
||||
|
||||
module.exports = {
|
||||
lintOnSave: false,
|
||||
configureWebpack: {
|
||||
// NOTE uncomment to use rust wasm
|
||||
// optimization: {
|
||||
// minimizer: [new OptimizeWasmPlugin()]
|
||||
// },
|
||||
// plugins: [
|
||||
// new WasmPackPlugin({
|
||||
// crateDirectory: path.resolve(__dirname, "crate")
|
||||
// })
|
||||
// ]
|
||||
},
|
||||
pluginOptions: {
|
||||
electronBuilder: {
|
||||
builderOptions: {
|
||||
// TODO electron-builder config https://www.electron.build/configuration/configuration
|
||||
appId: "com.crimata.messenger",
|
||||
productName: "Crimata-Messenger"
|
||||
},
|
||||
preload: "src/preload.ts"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -1 +0,0 @@
|
|||
{"w":735,"h":510,"x":1354,"y":513}
|
||||
Loading…
Reference in a new issue