diff --git a/.browserslistrc b/.browserslistrc new file mode 100644 index 0000000..214388f --- /dev/null +++ b/.browserslistrc @@ -0,0 +1,3 @@ +> 1% +last 2 versions +not dead diff --git a/.gitignore b/.gitignore index 20128fc..67a7ab0 100644 --- a/.gitignore +++ b/.gitignore @@ -9,7 +9,6 @@ crash.log # local env files .env.local .env.*.local -.env # Log files npm-debug.log* @@ -28,10 +27,3 @@ pnpm-debug.log* #Electron-builder output /dist_electron - -#Electron-builder config -electron-builder.yml - -#Window and session states -session.json -window.json diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml deleted file mode 100644 index 2c0d22c..0000000 --- a/.gitlab-ci.yml +++ /dev/null @@ -1,63 +0,0 @@ -stages: - - Build - - Upload - - Release - -build: - stage: Build - tags: - # Use a macos runner to build. - - darwin - before_script: - - yarn - script: - - export VERSION=$(node -e "console.log(require('./package.json').version)") - - echo "VERSION=$VERSION" >> variables.env - - export APPNAME=$(node -e "console.log(require('./package.json').productName)") - - echo "APPNAME=$APPNAME" >> variables.env - - yarn build - artifacts: - reports: - dotenv: variables.env - name: $CI_COMMIT_REF_SLUG - paths: - - dist_electron/*.dmg - - dist_electron/*.zip - - dist_electron/*.yml - when: on_success - only: - - main - -variables: - PACKAGE: '${APPNAME}-${VERSION}.dmg' - PACKAGE_REGISTRY_URL: '${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/${APPNAME}/${VERSION}' - -# Upload package to gitlab registry. -upload: - stage: Upload - needs: - - job: build - artifacts: true - rules: - - if: $CI_COMMIT_TAG - when: never # Do not run this job when a tag is created manually - - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH # Run this job when when commits are pushed to the default branch - script: - # Take the package we built and place it in the package registry. - - 'curl --header "JOB-TOKEN: $CI_JOB_TOKEN" --upload-file "dist_electron/${PACKAGE}" "${PACKAGE_REGISTRY_URL}/${PACKAGE}"' - -auto-release-master: - image: registry.gitlab.com/gitlab-org/release-cli - needs: - - job: build - artifacts: true - - job: upload - artifacts: true - stage: Release - rules: - - if: $CI_COMMIT_TAG - when: never # Do not run this job when a tag is created manually - - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH # Run this job when when commits are pushed to the default branch - script: - - echo "Release $VERSION" - - release-cli create --name "Release $VERSION" --tag-name v$VERSION --description "Release $CI_COMMIT_TITLE" --ref $CI_COMMIT_SHA --assets-link "{\"name\":\"${APPNAME}\",\"url\":\"${PACKAGE_REGISTRY_URL}/${PACKAGE}\"}" diff --git a/README.md b/README.md index c13a8ab..6fb4b27 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ yarn install ### Compiles and hot-reloads for development ``` -yarn electron:serve +yarn dev ``` ### Compiles and minifies for production @@ -15,7 +15,7 @@ yarn electron:serve yarn electron:build ``` - +``` ### Customize configuration See [Configuration Reference](https://cli.vuejs.org/config/). diff --git a/build/config.gypi b/build/config.gypi new file mode 100644 index 0000000..6f84ed7 --- /dev/null +++ b/build/config.gypi @@ -0,0 +1,79 @@ +# Do not edit. File was generated by node-gyp's "configure" step +{ + "target_defaults": { + "cflags": [], + "default_configuration": "Release", + "defines": [], + "include_dirs": [], + "libraries": [] + }, + "variables": { + "asan": 0, + "build_v8_with_gn": "false", + "coverage": "false", + "dcheck_always_on": 0, + "debug_nghttp2": "false", + "debug_node": "false", + "enable_lto": "false", + "enable_pgo_generate": "false", + "enable_pgo_use": "false", + "error_on_warn": "false", + "force_dynamic_crt": 0, + "host_arch": "x64", + "icu_data_in": "../../deps/icu-tmp/icudt67l.dat", + "icu_endianness": "l", + "icu_gyp_path": "tools/icu/icu-generic.gyp", + "icu_path": "deps/icu-small", + "icu_small": "false", + "icu_ver_major": "67", + "is_debug": 0, + "llvm_version": "0.0", + "napi_build_version": "6", + "node_byteorder": "little", + "node_debug_lib": "false", + "node_enable_d8": "false", + "node_install_npm": "true", + "node_module_version": 83, + "node_no_browser_globals": "false", + "node_prefix": "/", + "node_release_urlbase": "https://nodejs.org/download/release/", + "node_shared": "false", + "node_shared_brotli": "false", + "node_shared_cares": "false", + "node_shared_http_parser": "false", + "node_shared_libuv": "false", + "node_shared_nghttp2": "false", + "node_shared_openssl": "false", + "node_shared_zlib": "false", + "node_tag": "", + "node_target_type": "executable", + "node_use_bundled_v8": "true", + "node_use_dtrace": "true", + "node_use_etw": "false", + "node_use_node_code_cache": "true", + "node_use_node_snapshot": "true", + "node_use_openssl": "true", + "node_use_v8_platform": "true", + "node_with_ltcg": "false", + "node_without_node_options": "false", + "openssl_fips": "", + "openssl_is_fips": "false", + "shlib_suffix": "83.dylib", + "target_arch": "x64", + "v8_enable_31bit_smis_on_64bit_arch": 0, + "v8_enable_gdbjit": 0, + "v8_enable_i18n_support": 1, + "v8_enable_inspector": 1, + "v8_enable_pointer_compression": 0, + "v8_no_strict_aliasing": 1, + "v8_optimized_debug": 1, + "v8_promise_internal_field_count": 1, + "v8_random_seed": 0, + "v8_trace_maps": 0, + "v8_use_siphash": 1, + "want_separate_host_toolset": 0, + "xcode_version": "11.0", + "nodedir": "/Users/Enrique/Library/Caches/node-gyp/14.4.0", + "standalone_static_library": 1 + } +} diff --git a/gcloud.json b/gcloud.json new file mode 100644 index 0000000..5c68549 --- /dev/null +++ b/gcloud.json @@ -0,0 +1,12 @@ +{ + "type": "service_account", + "project_id": "crimata", + "private_key_id": "3a324fa3fcca4cea589e4b5007f4142db4b3a84d", + "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCxFJOjhIkYlyhn\nHjtpFC5EOwzNFZem6k6SS7zcgWbNLe9mcBsiTKCjpxItzkIo0pnT32yqw+AKsqpv\n6+LVyzzjfB7vtEUflvg6KgmYMS4ixOTCEbN3e2qj1t2RnITXZR2VGSGdoKKyqZm0\ntWrmexQvSN20cByL1vfxD3E69oYFGhVraa0zmm94WXHAzofT6DkvGdrbK1s1G9jI\nG3mdQ1jN71tghyloeHL+R/1iKQoZFYTkVe6yWvccSNAZelt/5sciCKRTCArXj89X\nLwU6jGOKIsyqx3BeUyWaSS59xpnGX3meSf5Q9U+j45ePUiqO6TTleF+kWiBwjUpp\nfeYcwEIDAgMBAAECggEAElkN9R7p75zV6F1XDYu0QYiWyncmx/o2Gu1zC6vywWa1\ni/korpSe/mX0ub9J1p3/p1balRUHlUQu6brYvYs56140fGTC1sOXQ7uQU+8glySs\niTk5TbOBeKluOsSrdP/6oTTB6Wm4AegVz4YOpgPxsvaLVkNZidnDxfvyIQhjGYsO\nsvRaw48eEmGOa1uvDhgvPOxiDp5YEdxqSue/7ylzjBdKecDyLgTSDDf/p+FwIx5B\n2d/0uRgQBaAk6D/DNOvN8GrYyakFmlDjE72nrBGAJq50UyV6THOSDk+8cK+2QiS1\nEDCFufj+7Dk1EBfBFUFY8ZBB/pphpEWyLoFl7NzGCQKBgQDaj/KOOcj6BkkTj1WM\nRW3KhOE3nhsCQEgQGCflpycEAZj6J7ktBG2/4teDt9sr+Epx8ymnQCZa9pmysdrD\nU0aWku1qUx0IUKTtZbAEbB+AnrWYDYkIWU8kRUUvNHNTBg43VyuohV9wprPih138\nHWMeNac82/XICz2A3rv+Xkx8KwKBgQDPaaEEGWVs3nIhf/p3M3/xWNKt8fhAERpG\n0YpTjgHW3FQFYjiCTTIcuLO3qsDGmfIAB9tL37s2Y3e2wKBGUj/o69Fr7sWVPdsX\nJtUc0+KiG2Bs0hsyZPdk9UQiQX8y2eq1NZW7vEoxWso1bM64WURB5N8ocWePNJJx\nL2Oe75rtiQKBgQDLfQQOiRxmNF3rOSMkAywyRr9NQgXRdbniSiszNQotP7OHDF7q\n29m2suOGfjIv4O6m4wdf8WkEfd4hsleETc9Ft6wVtyYrrLGxWWCk7WnzHVDjLY7s\n2AHIOjostf+9R8EKoz1BnFN8laibev71EQNMiBWZow1VX6m2hymurWs2mwKBgQCb\nuY3n2v14qOb92e1+U89KsEq1yMd/qpeU9jwqEaO14wS+aglNY5ItWEuuqWhFdE3q\n0ftHUzpnUnUOZD+xrI1JXsyEgegc7i0xi7lUBI3S8kUKTxGWW5IXXcKDCbPrxQtg\ndFPweSUnOyg4xnHKnVMPOjyGS+bZ8TnF+zOLoBAtKQKBgF6hAOIwF1DheXT4p8xS\nlSGhl8dBxjsJDVwbnFWQceGzGnq1e3ncy/UItt42UCDVRWTH4r7pQ9eKyd/3foUD\n6KDIcCrYo2KJh6ZNFJ67XF30U+TuEg5U/9jTbWqDOwYVXcMcnzQE4ZiGBxVHL6xr\nNc8FRW5+7xh4zNfzhGUGt5kt\n-----END PRIVATE KEY-----\n", + "client_email": "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" +} diff --git a/login.svg b/login.svg new file mode 100644 index 0000000..a2ccbf3 --- /dev/null +++ b/login.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + Email + Password + + Login + + Submit + Create account + + \ No newline at end of file diff --git a/package.json b/package.json index 24d13b1..5bb4571 100644 --- a/package.json +++ b/package.json @@ -1,18 +1,24 @@ { "name": "Crimata", - "version": "0.9.9", - "private": true, + "productName": "crimata-messenger", "description": "Cross-platform messenger application built with electron, vue3, and TS.", + "version": "0.1.0", + "private": true, "author": { "name": "Enrique Hernandez" }, "scripts": { - "build": "vue-cli-service electron:build", - "dev": "vue-cli-service electron:serve", + "dev": "yarn electron:serve", + "serve": "vue-cli-service serve", + "build": "vue-cli-service build", + "test:unit": "vue-cli-service test:unit", + "lint": "vue-cli-service lint", + "electron:build": "vue-cli-service electron:build", + "electron:serve": "vue-cli-service electron:serve", "postinstall": "electron-builder install-app-deps", - "postuninstall": "electron-builder install-app-deps" + "postuninstal": "electron-builder install-app-deps" }, - "main": "init.js", + "main": "background.js", "dependencies": { "@google-cloud/speech": "^4.2.0", "@types/animejs": "^3.1.2", @@ -22,15 +28,10 @@ "@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", @@ -38,8 +39,6 @@ "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", @@ -56,36 +55,17 @@ "@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.6", + "vue-cli-plugin-electron-builder": "~2.0.0-rc.4", "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" }, @@ -94,11 +74,5 @@ "vue-cli-service lint", "git add" ] - }, - "productName": "crimata-messenger", - "repository": { - "type": "git", - "url": "https://gitlab.com/crimata/electron-app.git", - "release": "latest" } } diff --git a/public/index.html b/public/index.html index 8f79d27..48809d8 100644 --- a/public/index.html +++ b/public/index.html @@ -11,7 +11,11 @@ -
+ +
diff --git a/src/App.vue b/src/App.vue new file mode 100644 index 0000000..165d108 --- /dev/null +++ b/src/App.vue @@ -0,0 +1,142 @@ + + + + + diff --git a/src/account.ts b/src/account.ts deleted file mode 100644 index 581da0e..0000000 --- a/src/account.ts +++ /dev/null @@ -1,95 +0,0 @@ - -import { postAuth, postLogin, postLogout } from "@/api/account"; -import { endSession, launchSession } from "@/session"; -import { getToken, setToken, setProfile, getProfile, clearStore } from "./store"; -import { parseAuthRes } from "./auth"; -import { ipcEmit } from "@/composables/useEmitter"; - -export const accountAuth = async (): Promise => { - - /* 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 = 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 => { - - 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 - -} - diff --git a/src/api/account.ts b/src/api/account.ts deleted file mode 100644 index 45baca2..0000000 --- a/src/api/account.ts +++ /dev/null @@ -1,31 +0,0 @@ - -import useHttp from "@/composables/useHttp"; -import axios from "axios"; -import {config} from "@/config"; - -const { post } = useHttp(); - -export const postAuth = async (token: string) => ( - await axios({ - url: config.BUSINESS_URL + config.BUSINESS_PREFIX + '/account/authenticate', - headers: { - Cookie: `crimataCookie=${token}` - }, - method: 'POST', - }) -); - - -export const postLogin = async (email: string, password: string) => ( - await post('/account/login', { email, password }) -); - - -export const postLogout = - async (): Promise => (await post('/account/logout')); - - - - - - diff --git a/src/assets/crimata.svg b/src/assets/crimata.svg new file mode 100644 index 0000000..8bb28dc --- /dev/null +++ b/src/assets/crimata.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/src/audio.ts b/src/audio.ts deleted file mode 100644 index 7ae2562..0000000 --- a/src/audio.ts +++ /dev/null @@ -1,187 +0,0 @@ -/* eslint @typescript-eslint/no-var-requires: "off" */ - -"use strict"; - -// where the audio goes -let buffer: ArrayBuffer[] = []; - -// place audio data in buffer -export const collect: IpcListenerCallback = (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 => ( - -// 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 { -// 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; -// } -// } -// } - - diff --git a/src/auth.ts b/src/auth.ts deleted file mode 100644 index 14ed36a..0000000 --- a/src/auth.ts +++ /dev/null @@ -1,13 +0,0 @@ - -export const parseAuthRes = (authRes: any) => { - const token = authRes.headers['set-cookie'][0].split(";")[0].split("=")[1] as string; - const profile = authRes.data as Profile; - return { - token, - profile - } -}; - - - - diff --git a/src/background.ts b/src/background.ts new file mode 100644 index 0000000..065b4ec --- /dev/null +++ b/src/background.ts @@ -0,0 +1,25 @@ +/* + * Entry point for Crimata electron app. + * "Look on my Works, ye Mighty, and despair!" + */ + +"use strict"; + +import { protocol } from "electron"; +import { initApp } from './background/init'; + +// Scheme must be registered before the app is ready +protocol.registerSchemesAsPrivileged([ + { scheme: "app", privileges: { secure: true, standard: true } } +]); + +// Load environment variable +const isDevelopment = process.env.NODE_ENV !== "production"; + +// NOTE Program Begins Here +(async () => { + + console.log('Starting Crimata electron app.'); + initApp(isDevelopment); + +})(); diff --git a/src/background/audio.ts b/src/background/audio.ts new file mode 100644 index 0000000..5b0ef12 --- /dev/null +++ b/src/background/audio.ts @@ -0,0 +1,135 @@ +/* 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 { + 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; + } +} + diff --git a/src/background/emitter.ts b/src/background/emitter.ts new file mode 100644 index 0000000..14aa77f --- /dev/null +++ b/src/background/emitter.ts @@ -0,0 +1,6 @@ +/* eslint-disable */ +const EventEmitter = require('events'); + +class BackgroundMitt extends EventEmitter { } + +export const backgroundMitt = new BackgroundMitt(); diff --git a/src/background/init.ts b/src/background/init.ts new file mode 100644 index 0000000..0f60068 --- /dev/null +++ b/src/background/init.ts @@ -0,0 +1,50 @@ + +"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(); + }); + } + } +} + diff --git a/src/background/run.ts b/src/background/run.ts new file mode 100644 index 0000000..55aad46 --- /dev/null +++ b/src/background/run.ts @@ -0,0 +1,28 @@ +"use strict"; + +import { createWindow } from './window'; +import { initSession } from './session'; +import { initAudioIO } from './audio'; + +let socket = false; + +/* + * The main function will be run after electron app is ready. + */ +export async function main() { + + // create main window. + await createWindow({ + width: 600, + height: 500, + resizable: true + }); + + // Instantiate socket session with crimata-platorm. + if (!socket) initSession(); + socket = true; + + // Begin audio stream. + initAudioIO(); + +} diff --git a/src/background/session.ts b/src/background/session.ts new file mode 100644 index 0000000..0f71cf9 --- /dev/null +++ b/src/background/session.ts @@ -0,0 +1,163 @@ +"use strict"; + +import WebSocket from 'ws'; +import { backgroundMitt } from './emitter'; +import { ipcMain } from "electron"; +import { play } from './audio'; +import { UserCreds, ClientMessage, RenderMessage, Annotation } 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 | UserCreds | null): Promise => { + return new Promise((resolve, reject) => { + + console.log('authenticating...'); + + backgroundMitt.once('auth-res', (res: string) => { + + if (res === 'locked') { + reject(res); + } else { + console.log('Success!'); + auth = true; + resolve(res); + } + + }); + + if (!payload) { + socket.send('no-token'); + } else if (payload instanceof String || typeof payload === 'string') { + socket.send(payload); + } else { + socket.send(JSON.stringify(payload)); + } + + }); +}; + +const onMessage = (messageStr: string): void => { + + while (!auth) { + backgroundMitt.emit('auth-res', messageStr); + return; + } + + // Decode the message. + const m = JSON.parse(messageStr) + let message: RenderMessage | Annotation; + + // Check to see if this is a Render Message. + if (m.content) { + message = renderMessage({ + text: m.content.text, + audio: m.content.audio, + context: m.context, + modifier: 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); + } + + } + + else { + message = m // Annotation + } + + // Send it to the frontend. + backgroundMitt.emit('ipc-renderer', { + endpoint: 'render-message', + message: 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.'); + + // call auth renderer event on connection + backgroundMitt.emit('ipc-renderer', { + endpoint: 'auth', + message: null + }); + + success = true; + + }); + + // Handle for failed connect, try again. + socket.on('error', (_e) => { + console.log('ERROR: Failed to connect.'); + socket.removeAllListeners(); + socket.close(); + setTimeout(() => { + if (!success) { + console.log('Reconnecting...'); + initSession(); + } + }, reconnectTimeout); + + }); + + socket.on('close', () => { + console.log('Connection droped! Restarting...'); + restart(); + }); + + socket.on("message", onMessage); +} + + +export function sendAudio(audio: string, uid: string): void { + const m = clientMessage("", audio, uid); + socket.send(JSON.stringify(m)); +} \ No newline at end of file diff --git a/src/background/window.ts b/src/background/window.ts new file mode 100644 index 0000000..27aac38 --- /dev/null +++ b/src/background/window.ts @@ -0,0 +1,109 @@ +"use strict"; + +import { BrowserWindow, ipcMain } from "electron"; +import { createProtocol } from "vue-cli-plugin-electron-builder/lib"; +import { backgroundMitt } from './emitter'; +import { RenderMessage } from "@/types/message/index"; +import * as path from "path"; + +interface WindowSettings { + width: number; + height: number; + resizable: boolean; +} + +interface IpcRendererPayload { + endpoint: string; + message: RenderMessage | null; +} + +let win: BrowserWindow | null; + +// Util function to handle window close & minimize. +const navBarHandler = (_event, action: string): void => { + switch(action) { + case 'close': + if (win) win.close(); + break; + case 'min': + if (win) win.minimize(); + break; + } +} + +// Util function to render message on ipc-renderer event. +const renderMessage = (payload: IpcRendererPayload): void => { + if (win) { + win.webContents.send(payload.endpoint, { + message: payload.message + }); + } +} + +// Do this on window mount. +const windowMount = (): void => { + backgroundMitt.emit('window-active', true); + // handle win nav-bar event. + ipcMain.removeHandler('nav-bar'); // avoid setting duplicate handlers + ipcMain.handle('nav-bar', navBarHandler); + // render messages through ipc-renderer. + backgroundMitt.on('ipc-renderer', renderMessage); +} + +// do this on window dismount. +const windowDismount = (): void => { + win = null; + backgroundMitt.emit('window-active', false); +} + +// function used by run.ts to create the main window. +export async function createWindow(options: WindowSettings): Promise { + return new Promise((resolve, _reject) => { + + // avoid creating duplicate windows. + if (win) resolve(); + + win = new BrowserWindow({ + width: options.width, + height: options.height, + resizable: options.resizable, + backgroundColor: '#EBEBEB', + frame: false, + minWidth: 350, + minHeight: 500, + webPreferences: { + // Use pluginOptions.nodeIntegration, leave this alone + // See nklayman.github.io/vue-cli-plugin-electron-builder/guide/security.html#node-integration for more info + nodeIntegration: (process.env + .ELECTRON_NODE_INTEGRATION as unknown) as boolean, + preload: path.join(__dirname, "preload.js") + } + }); + + + if (process.env.WEBPACK_DEV_SERVER_URL) { + // Load the url of the dev server if in development mode + win.loadURL(process.env.WEBPACK_DEV_SERVER_URL as string); + } else { + createProtocol("app"); + // Load the index.html when not in development + win.loadURL("app://./index.html"); + } + + // Handle window close. + win.on("closed", windowDismount); + + win.once('ready-to-show', () => { + if (win) win.show() + }) + + win.webContents.on('did-finish-load', () => { + if (win) win.webContents.send('window-ready', { + message: true + }); + windowMount(); + resolve(); + }); + + }); +} diff --git a/src/components/inputItem/inputController.ts b/src/components/inputItem/inputController.ts new file mode 100644 index 0000000..0ef68ee --- /dev/null +++ b/src/components/inputItem/inputController.ts @@ -0,0 +1,189 @@ +/* +* Main logic used by inputItem component. +* Will visualize text input by default as user types. +* If user holds space bar down, input audio stream will +* be recorded and sent to the backend for analysis. +*/ + +import { ref, watch, onUnmounted, onMounted } from "vue"; +import useMitt from "@/modules/mitt"; +import { useIpc } from '@/modules/ipc'; +import keyboardNameMap from "./keyBoardMaps/keyboardNameMap"; +import keyboardCharMap from "./keyBoardMaps/keyboardCharMap"; +import anime from "animejs"; +import { clientMessage, renderMessage } from '@/modules/message'; + +let textInput: HTMLInputElement | null; + +// Animation functions for text input element. +function expandInput() { + anime({ + targets: textInput, + width: ['0px', '150px'], + duration: 500, + easing: 'easeOutExpo', + }) +} + +function retractInput() { + anime({ + targets: textInput, + width: ['150px', '0px'], + duration: 500, + easing: 'easeOutExpo', + begin: function() { + if (textInput) textInput.blur(); + } + }) +} + +export default function useInputController() { + + const { emitter } = useMitt(); + const { post } = useIpc(); + const isRecording = ref(false); + const isTyping = ref(false); + const input = ref(""); + const capsLock = ref(false); + + // Utility function to render message. + const render = (text: string, audio: boolean | string, context: string, modifier: string): string => { + const m = renderMessage({ text, audio, context, modifier }); + emitter.emit("self-message", m); + console.log(m.time) + return m.uid; + } + + // send message on ENTER. + const onEnter = () => { + + if (input.value) { + + // Render the message immediately and get its unique id. + const uid = render(input.value, false, "", "sf"); + + // Then send it to the backend for processing. + const clientM = clientMessage(input.value, false, uid); + post('client-message', clientM); + + // Reset the text input. + if (textInput) textInput.value = ''; + input.value = " "; + } + + } + + // update input on keydown event & listen for special commands. + const onKeyDown = (e: KeyboardEvent) => { + if (textInput) { + textInput.focus(); + textInput.value = input.value; + } + + // keyboardCharMap is an array of arrays, with each inner + // array having 2 columns - un-shifted, and shifted values. + // iCol = 0 is the un-shifted value, while iCol=1 is the shifted value. + // See the "keyboardCharMap" module for possible keys. + // MODIFY keyboardCharMap to suit your needs if you want + // more/less/different characters to be considered "printable". + let iCol = 0; + if (e.shiftKey || capsLock.value) { + iCol = 1; + } + + const ch = keyboardCharMap[e.keyCode][iCol]; + + // Optionally do things with non-printables, + // like CR, ESC, Backspace, LF, Tab, Arrows, F1-F24, etc. + // See the arrary "keyboardNameMap" module for possible keys. + const cmd = keyboardNameMap[e.keyCode]; + + switch (cmd) { + case "ENTER": + onEnter(); + break; + case "BACK_SPACE": + input.value = input.value.slice(0, -1); + break; + case "CAPS_LOCK": + capsLock.value = true; + break; + case "ESCAPE": + if (textInput) textInput.value = '' + input.value = ""; + break; + default: + input.value += ch; + input.value.trim(); + break; + } + + } + + // stop stream recording and render audio message on space key up event. + const onKeyUp = (e: KeyboardEvent) => { + if (e.key === " " && isRecording.value) { + + isRecording.value = false; + + // // Render the message immediately and get its unique id. + const uid = render("", "", "", "sf"); + // Notify main process to stop recording and send send audio to backend for analysis. + post('update-recorder', { + isRecording: isRecording.value, + uid + }); + + // Reset input. + input.value = '' + } + else if (e.code === "CapsLock") { + capsLock.value = false; + } + } + + onMounted(() => { + textInput = document.getElementById('textInput') as HTMLInputElement; + // add window event keyboard listeners + window.addEventListener("keydown", onKeyDown); + window.addEventListener('keyup', onKeyUp); + }) + + onUnmounted(() => { + window.removeEventListener("keydown", onKeyDown); + window.removeEventListener("keyup", onKeyUp); + }); + + // watch keyboard input & update inputItem state. + watch(input, (input, prevInput) => { + if (isTyping.value && input.length === 0) { + retractInput(); + } + + // Do nothing if user is typing. + if (prevInput !== "" || prevInput.length > 0) { + return; + } + + // Begin stream recording on space bar. + if (input === " " && isRecording.value === false) { + isTyping.value = false; + isRecording.value = true; + // tell main process to begin stream recording. + post('update-recorder', { + isRecording: isRecording.value, + uid: null + }); + return; + } + + isTyping.value = true; + expandInput() + }); + + return { + isRecording, + isTyping + } + +} diff --git a/src/render/components/inputItem.vue b/src/components/inputItem/inputItem.vue similarity index 61% rename from src/render/components/inputItem.vue rename to src/components/inputItem/inputItem.vue index edd844f..e7723b9 100644 --- a/src/render/components/inputItem.vue +++ b/src/components/inputItem/inputItem.vue @@ -2,95 +2,140 @@
-
{{ initials }}
- - - - + + + +
+
- + - - +
diff --git a/src/components/inputItem/keyBoardMaps/keyboardCharMap.ts b/src/components/inputItem/keyBoardMaps/keyboardCharMap.ts new file mode 100644 index 0000000..102e9c3 --- /dev/null +++ b/src/components/inputItem/keyBoardMaps/keyboardCharMap.ts @@ -0,0 +1,263 @@ +// This has the UnShifted and Shifted characters that each key maps to +// Ones that are to be ignored for character input are empty. +const keyboardCharMap = [ + ["", ""], // [0] + ["", ""], // [1] + ["", ""], // [2] + ["", ""], // [3] + ["", ""], // [4] + ["", ""], // [5] + ["", ""], // [6] + ["", ""], // [7] + ["", ""], // [8] + ["", ""], // [9] + ["", ""], // [10] + ["", ""], // [11] + ["", ""], // [12] + ["\r", "\r"], // [13] - MOST control characters are ignored. This one (Carriage Return, or "Enter") is significant! + ["", ""], // [14] + ["", ""], // [15] + ["", ""], // [16] + ["", ""], // [17] + ["", ""], // [18] + ["", ""], // [19] + ["", ""], // [20] + ["", ""], // [21] + ["", ""], // [22] + ["", ""], // [23] + ["", ""], // [24] + ["", ""], // [25] + ["", ""], // [26] + ["", ""], // [27] + ["", ""], // [28] + ["", ""], // [29] + ["", ""], // [30] + ["", ""], // [31] + [" ", " "], // [32] // SPACE! Don't "clean it up" and remove the space! + ["", ""], // [33] + ["", ""], // [34] + ["", ""], // [35] + ["", ""], // [36] + ["", ""], // [37] + ["", ""], // [38] + ["", ""], // [39] + ["", ""], // [40] + ["", ""], // [41] + ["", ""], // [42] + ["", ""], // [43] + ["", ""], // [44] + ["", ""], // [45] + ["", ""], // [46] + ["", ""], // [47] + ["0", ")"], // [48] + ["1", "!"], // [49] + ["2", "@"], // [50] + ["3", "#"], // [51] + ["4", "$"], // [52] + ["5", "%"], // [53] + ["6", "^"], // [54] + ["7", "&"], // [55] + ["8", "*"], // [56] + ["9", "("], // [57] + ["", ""], // [58] + [";", ":"], // [59] + ["<", ""], // [60] + ["=", ""], // [61] + [">", ""], // [62] + ["?", ""], // [63] shifted; else "/" + ["", ""], // [64] + ["a", "A"], // [65] + ["b", "B"], // [66] + ["c", "C"], // [67] + ["d", "D"], // [68] + ["e", "E"], // [69] + ["f", "F"], // [70] + ["g", "G"], // [71] + ["h", "H"], // [72] + ["i", "I"], // [73] + ["j", "J"], // [74] + ["k", "K"], // [75] + ["l", "L"], // [76] + ["m", "M"], // [77] + ["n", "N"], // [78] + ["o", "O"], // [79] + ["p", "P"], // [80] + ["q", "Q"], // [81] + ["r", "R"], // [82] + ["s", "S"], // [83] + ["t", "T"], // [84] + ["u", "U"], // [85] + ["v", "V"], // [86] + ["w", "W"], // [87] + ["x", "X"], // [88] + ["y", "Y"], // [89] + ["z", "Z"], // [90] + ["", ""], // [91] Windows Key (Windows) or Command Key (Mac) + ["", ""], // [92] + ["", ""], // [93] + ["", ""], // [94] + ["", ""], // [95] + // Number Keypad Entries... + ["0", ""], // [96] + ["1", ""], // [97] + ["2", ""], // [98] + ["3", ""], // [99] + ["4", ""], // [100] + ["5", ""], // [101] + ["6", ""], // [102] + ["7", ""], // [103] + ["8", ""], // [104] + ["9", ""], // [105] + ["*", ""], // [106] + ["+", ""], // [107] + ["", ""], // [108] + ["-", ""], // [109] + [".", ""], // [110] + ["/", ""], // [111] + + ["", ""], // [112] + ["", ""], // [113] + ["", ""], // [114] + ["", ""], // [115] + ["", ""], // [116] + ["", ""], // [117] + ["", ""], // [118] + ["", ""], // [119] + ["", ""], // [120] + ["", ""], // [121] + ["", ""], // [122] + ["", ""], // [123] + ["", ""], // [124] + ["", ""], // [125] + ["", ""], // [126] + ["", ""], // [127] + ["", ""], // [128] + ["", ""], // [129] + ["", ""], // [130] + ["", ""], // [131] + ["", ""], // [132] + ["", ""], // [133] + ["", ""], // [134] + ["", ""], // [135] + ["", ""], // [136] + ["", ""], // [137] + ["", ""], // [138] + ["", ""], // [139] + ["", ""], // [140] + ["", ""], // [141] + ["", ""], // [142] + ["", ""], // [143] + ["", ""], // [144] + ["", ""], // [145] + ["", ""], // [146] + ["", ""], // [147] + ["", ""], // [148] + ["", ""], // [149] + ["", ""], // [150] + ["", ""], // [151] + ["", ""], // [152] + ["", ""], // [153] + ["", ""], // [154] + ["", ""], // [155] + ["", ""], // [156] + ["", ""], // [157] + ["", ""], // [158] + ["", ""], // [159] + ["", ""], // [160] + ["", ""], // [161] + ["", ""], // [162] + ["", ""], // [163] + ["", ""], // [164] + ["", ""], // [165] + ["", ""], // [166] + ["", ""], // [167] + ["", ""], // [168] + ["", ""], // [169] + ["", ""], // [170] + ["", ""], // [171] + ["", ""], // [172] + ["", ""], // [173] + ["", ""], // [174] + ["", ""], // [175] + ["", ""], // [176] + ["", ""], // [177] + ["", ""], // [178] + ["", ""], // [179] + ["", ""], // [180] + ["", ""], // [181] + ["", ""], // [182] + ["", ""], // [183] + ["", ""], // [184] + ["", ""], // [185] + [";", ":"], // [186] + ["=", "+"], // [187] + [",", "<"], // [188] + ["-", "_"], // [189] + [".", ">"], // [190] + ["/", "?"], // [191] + ["`", "~"], // [192] + ["", ""], // [193] + ["", ""], // [194] + ["", ""], // [195] + ["", ""], // [196] + ["", ""], // [197] + ["", ""], // [198] + ["", ""], // [199] + ["", ""], // [200] + ["", ""], // [201] + ["", ""], // [202] + ["", ""], // [203] + ["", ""], // [204] + ["", ""], // [205] + ["", ""], // [206] + ["", ""], // [207] + ["", ""], // [208] + ["", ""], // [209] + ["", ""], // [210] + ["", ""], // [211] + ["", ""], // [212] + ["", ""], // [213] + ["", ""], // [214] + ["", ""], // [215] + ["", ""], // [216] + ["", ""], // [217] + ["", ""], // [218] + ["[", "{"], // [219] + ["\\", "|"], // [220] + ["]", "}"], // [221] + ["'", '"'], // [222] + ["", ""], // [223] + ["", ""], // [224] + ["", ""], // [225] + ["", ""], // [226] + ["", ""], // [227] + ["", ""], // [228] + ["", ""], // [229] + ["", ""], // [230] + ["", ""], // [231] + ["", ""], // [232] + ["", ""], // [233] + ["", ""], // [234] + ["", ""], // [235] + ["", ""], // [236] + ["", ""], // [237] + ["", ""], // [238] + ["", ""], // [239] + ["", ""], // [240] + ["", ""], // [241] + ["", ""], // [242] + ["", ""], // [243] + ["", ""], // [244] + ["", ""], // [245] + ["", ""], // [246] + ["", ""], // [247] + ["", ""], // [248] + ["", ""], // [249] + ["", ""], // [250] + ["", ""], // [251] + ["", ""], // [252] + ["", ""], // [253] + ["", ""], // [254] + ["", ""] // [255] +]; +export default keyboardCharMap; diff --git a/src/components/inputItem/keyBoardMaps/keyboardNameMap.ts b/src/components/inputItem/keyBoardMaps/keyboardNameMap.ts new file mode 100644 index 0000000..8f385ae --- /dev/null +++ b/src/components/inputItem/keyBoardMaps/keyboardNameMap.ts @@ -0,0 +1,261 @@ +// names of known key codes (0-255) +const keyboardNameMap = [ + "", // [0] + "", // [1] + "", // [2] + "CANCEL", // [3] + "", // [4] + "", // [5] + "HELP", // [6] + "", // [7] + "BACK_SPACE", // [8] + "TAB", // [9] + "", // [10] + "", // [11] + "CLEAR", // [12] + "ENTER", // [13] + "ENTER_SPECIAL", // [14] + "", // [15] + "SHIFT", // [16] + "CONTROL", // [17] + "ALT", // [18] + "PAUSE", // [19] + "CAPS_LOCK", // [20] + "KANA", // [21] + "EISU", // [22] + "JUNJA", // [23] + "FINAL", // [24] + "HANJA", // [25] + "", // [26] + "ESCAPE", // [27] + "CONVERT", // [28] + "NONCONVERT", // [29] + "ACCEPT", // [30] + "MODECHANGE", // [31] + "SPACE", // [32] + "PAGE_UP", // [33] + "PAGE_DOWN", // [34] + "END", // [35] + "HOME", // [36] + "LEFT", // [37] + "UP", // [38] + "RIGHT", // [39] + "DOWN", // [40] + "SELECT", // [41] + "PRINT", // [42] + "EXECUTE", // [43] + "PRINTSCREEN", // [44] + "INSERT", // [45] + "DELETE", // [46] + "", // [47] + "0", // [48] + "1", // [49] + "2", // [50] + "3", // [51] + "4", // [52] + "5", // [53] + "6", // [54] + "7", // [55] + "8", // [56] + "9", // [57] + "COLON", // [58] + "SEMICOLON", // [59] + "LESS_THAN", // [60] + "EQUALS", // [61] + "GREATER_THAN", // [62] + "QUESTION_MARK", // [63] + "AT", // [64] + "A", // [65] + "B", // [66] + "C", // [67] + "D", // [68] + "E", // [69] + "F", // [70] + "G", // [71] + "H", // [72] + "I", // [73] + "J", // [74] + "K", // [75] + "L", // [76] + "M", // [77] + "N", // [78] + "O", // [79] + "P", // [80] + "Q", // [81] + "R", // [82] + "S", // [83] + "T", // [84] + "U", // [85] + "V", // [86] + "W", // [87] + "X", // [88] + "Y", // [89] + "Z", // [90] + "OS_KEY", // [91] Windows Key (Windows) or Command Key (Mac) + "", // [92] + "CONTEXT_MENU", // [93] + "", // [94] + "SLEEP", // [95] + "NUMPAD0", // [96] + "NUMPAD1", // [97] + "NUMPAD2", // [98] + "NUMPAD3", // [99] + "NUMPAD4", // [100] + "NUMPAD5", // [101] + "NUMPAD6", // [102] + "NUMPAD7", // [103] + "NUMPAD8", // [104] + "NUMPAD9", // [105] + "MULTIPLY", // [106] + "ADD", // [107] + "SEPARATOR", // [108] + "SUBTRACT", // [109] + "DECIMAL", // [110] + "DIVIDE", // [111] + "F1", // [112] + "F2", // [113] + "F3", // [114] + "F4", // [115] + "F5", // [116] + "F6", // [117] + "F7", // [118] + "F8", // [119] + "F9", // [120] + "F10", // [121] + "F11", // [122] + "F12", // [123] + "F13", // [124] + "F14", // [125] + "F15", // [126] + "F16", // [127] + "F17", // [128] + "F18", // [129] + "F19", // [130] + "F20", // [131] + "F21", // [132] + "F22", // [133] + "F23", // [134] + "F24", // [135] + "", // [136] + "", // [137] + "", // [138] + "", // [139] + "", // [140] + "", // [141] + "", // [142] + "", // [143] + "NUM_LOCK", // [144] + "SCROLL_LOCK", // [145] + "WIN_OEM_FJ_JISHO", // [146] + "WIN_OEM_FJ_MASSHOU", // [147] + "WIN_OEM_FJ_TOUROKU", // [148] + "WIN_OEM_FJ_LOYA", // [149] + "WIN_OEM_FJ_ROYA", // [150] + "", // [151] + "", // [152] + "", // [153] + "", // [154] + "", // [155] + "", // [156] + "", // [157] + "", // [158] + "", // [159] + "CIRCUMFLEX", // [160] + "EXCLAMATION", // [161] + "DOUBLE_QUOTE", // [162] + "HASH", // [163] + "DOLLAR", // [164] + "PERCENT", // [165] + "AMPERSAND", // [166] + "UNDERSCORE", // [167] + "OPEN_PAREN", // [168] + "CLOSE_PAREN", // [169] + "ASTERISK", // [170] + "PLUS", // [171] + "PIPE", // [172] + "HYPHEN_MINUS", // [173] + "OPEN_CURLY_BRACKET", // [174] + "CLOSE_CURLY_BRACKET", // [175] + "TILDE", // [176] + "", // [177] + "", // [178] + "", // [179] + "", // [180] + "VOLUME_MUTE", // [181] + "VOLUME_DOWN", // [182] + "VOLUME_UP", // [183] + "", // [184] + "", // [185] + "SEMICOLON", // [186] + "EQUALS", // [187] + "COMMA", // [188] + "MINUS", // [189] + "PERIOD", // [190] + "SLASH", // [191] + "BACK_QUOTE", // [192] + "", // [193] + "", // [194] + "", // [195] + "", // [196] + "", // [197] + "", // [198] + "", // [199] + "", // [200] + "", // [201] + "", // [202] + "", // [203] + "", // [204] + "", // [205] + "", // [206] + "", // [207] + "", // [208] + "", // [209] + "", // [210] + "", // [211] + "", // [212] + "", // [213] + "", // [214] + "", // [215] + "", // [216] + "", // [217] + "", // [218] + "OPEN_BRACKET", // [219] + "BACK_SLASH", // [220] + "CLOSE_BRACKET", // [221] + "QUOTE", // [222] + "", // [223] + "META", // [224] + "ALTGR", // [225] + "", // [226] + "WIN_ICO_HELP", // [227] + "WIN_ICO_00", // [228] + "", // [229] + "WIN_ICO_CLEAR", // [230] + "", // [231] + "", // [232] + "WIN_OEM_RESET", // [233] + "WIN_OEM_JUMP", // [234] + "WIN_OEM_PA1", // [235] + "WIN_OEM_PA2", // [236] + "WIN_OEM_PA3", // [237] + "WIN_OEM_WSCTRL", // [238] + "WIN_OEM_CUSEL", // [239] + "WIN_OEM_ATTN", // [240] + "WIN_OEM_FINISH", // [241] + "WIN_OEM_COPY", // [242] + "WIN_OEM_AUTO", // [243] + "WIN_OEM_ENLW", // [244] + "WIN_OEM_BACKTAB", // [245] + "ATTN", // [246] + "CRSEL", // [247] + "EXSEL", // [248] + "EREOF", // [249] + "PLAY", // [250] + "ZOOM", // [251] + "", // [252] + "PA1", // [253] + "WIN_OEM_CLEAR", // [254] + "" // [255] +]; + +export default keyboardNameMap; diff --git a/src/render/components/bubble.vue b/src/components/messageItem.vue similarity index 58% rename from src/render/components/bubble.vue rename to src/components/messageItem.vue index 982cef3..63c58bd 100644 --- a/src/render/components/bubble.vue +++ b/src/components/messageItem.vue @@ -1,52 +1,111 @@ -