production refactor
38
.eslintrc.js
|
|
@ -1,38 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
root: true,
|
|
||||||
env: {
|
|
||||||
node: true
|
|
||||||
},
|
|
||||||
extends: [
|
|
||||||
"plugin:vue/vue3-essential",
|
|
||||||
"eslint:recommended",
|
|
||||||
"@vue/typescript/recommended"
|
|
||||||
],
|
|
||||||
parserOptions: {
|
|
||||||
ecmaVersion: 2020
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
|
|
||||||
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
|
|
||||||
"no-unused-vars": "off",
|
|
||||||
"@typescript-eslint/no-unused-vars": [
|
|
||||||
"error",
|
|
||||||
{ "argsIgnorePattern": "^_" }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
overrides: [
|
|
||||||
{
|
|
||||||
files: [
|
|
||||||
"**/__tests__/*.{j,t}s?(x)",
|
|
||||||
"**/tests/unit/**/*.spec.{j,t}s?(x)",
|
|
||||||
"*.js"
|
|
||||||
],
|
|
||||||
rules: {
|
|
||||||
"@typescript-eslint/no-var-requires": "off",
|
|
||||||
},
|
|
||||||
env: {
|
|
||||||
jest: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
12
.gitignore
vendored
|
|
@ -1,5 +1,5 @@
|
||||||
.DS_Store
|
.DS_Store
|
||||||
node_modules
|
/node_modules
|
||||||
/dist
|
/dist
|
||||||
/crate/target/*
|
/crate/target/*
|
||||||
/.log
|
/.log
|
||||||
|
|
@ -17,9 +17,6 @@ yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
pnpm-debug.log*
|
pnpm-debug.log*
|
||||||
|
|
||||||
# Yarn cache
|
|
||||||
/.yarn
|
|
||||||
|
|
||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
.idea
|
.idea
|
||||||
.vscode
|
.vscode
|
||||||
|
|
@ -31,10 +28,3 @@ pnpm-debug.log*
|
||||||
|
|
||||||
#Electron-builder output
|
#Electron-builder output
|
||||||
/dist_electron
|
/dist_electron
|
||||||
|
|
||||||
#Electron-builder config
|
|
||||||
electron-builder.yml
|
|
||||||
|
|
||||||
#Window and session states
|
|
||||||
session.json
|
|
||||||
window.json
|
|
||||||
|
|
|
||||||
2
.npmrc
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
@crimata:registry=https://gitlab.com/api/v4/projects/28849281/packages/npm/
|
||||||
|
//gitlab.com/api/v4/projects/28849281/packages/npm/:_authToken=${NPM_TOKEN}
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
nodeLinker: node-modules
|
|
||||||
|
|
||||||
npmScopes:
|
|
||||||
crimata:
|
|
||||||
npmAuthToken: "${NPM_TOKEN}"
|
|
||||||
npmRegistryServer: "https://gitlab.com/api/v4/projects/28849281/packages/npm/"
|
|
||||||
|
|
||||||
yarnPath: ".yarn/releases/yarn-berry.cjs"
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
presets: [
|
|
||||||
'@vue/cli-plugin-babel/preset'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
preset: '@vue/cli-plugin-unit-jest/presets/typescript-and-babel',
|
|
||||||
transform: {
|
|
||||||
'^.+\\.vue$': 'vue-jest'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
{
|
|
||||||
"typeAcquisition": {
|
|
||||||
|
|
||||||
"enable": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
2213
package-lock.json
generated
Normal file
93
package.json
|
|
@ -1,103 +1,24 @@
|
||||||
{
|
{
|
||||||
"name": "Crimata",
|
"name": "Crimata",
|
||||||
"productName": "crimata-messenger",
|
"productName": "crimata-messenger",
|
||||||
"version": "1.0.0-beta.0",
|
"version": "1.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Cross-platform messenger application built with electron, vue3, and TS.",
|
"description": "Cross-platform messenger app (Electron, Vue3, and TS)",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Andrew Gundersen"
|
"name": "Andrew Gundersen"
|
||||||
},
|
},
|
||||||
|
"main": "src/main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "vue-cli-service electron:build",
|
"start": "electron ."
|
||||||
"dev": "vue-cli-service electron:serve"
|
|
||||||
},
|
},
|
||||||
"main": "background.js",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@crimata/nodeaudio": "^0.0.0",
|
"@crimata/nodeaudio": "^0.0.0",
|
||||||
"@types/animejs": "^3.1.2",
|
|
||||||
"@types/bindings": "^1.3.0",
|
|
||||||
"@types/dom-mediacapture-record": "^1.0.7",
|
|
||||||
"@types/node": "^14.14.25",
|
|
||||||
"@types/uuid": "^8.3.0",
|
|
||||||
"@types/ws": "^7.2.7",
|
|
||||||
"animejs": "^3.2.0",
|
|
||||||
"axios": "^0.21.1",
|
"axios": "^0.21.1",
|
||||||
"core-js": "^3.6.5",
|
"base64-arraybuffer": "^1.0.1",
|
||||||
"electron-is-dev": "^2.0.0",
|
|
||||||
"electron-store": "^8.0.0",
|
"electron-store": "^8.0.0",
|
||||||
"electron-updater": "^4.3.8",
|
|
||||||
"fft.js": "^4.0.4",
|
|
||||||
"mitt": "^2.1.0",
|
|
||||||
"node-gyp": "^8.2.0",
|
|
||||||
"update-electron-app": "^2.0.1",
|
|
||||||
"vue": "^3.0.0-0",
|
|
||||||
"vue-router": "^4.0.0-0",
|
|
||||||
"vuex": "^4.0.0-0",
|
|
||||||
"ws": "^7.3.1"
|
"ws": "^7.3.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/axios": "^0.14.0",
|
"electron": "^13.2.3"
|
||||||
"@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",
|
|
||||||
"@vue/cli-plugin-babel": "~4.5.0",
|
|
||||||
"@vue/cli-plugin-eslint": "~4.5.0",
|
|
||||||
"@vue/cli-plugin-router": "~4.5.0",
|
|
||||||
"@vue/cli-plugin-typescript": "~4.5.0",
|
|
||||||
"@vue/cli-plugin-unit-jest": "~4.5.0",
|
|
||||||
"@vue/cli-plugin-vuex": "~4.5.0",
|
|
||||||
"@vue/cli-service": "~4.5.0",
|
|
||||||
"@vue/compiler-sfc": "^3.0.0-0",
|
|
||||||
"@vue/eslint-config-typescript": "^5.0.2",
|
|
||||||
"@vue/test-utils": "^2.0.0-0",
|
|
||||||
"@wasm-tool/wasm-pack-plugin": "^1.3.1",
|
|
||||||
"electron": "11.4.10",
|
|
||||||
"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",
|
|
||||||
"typescript": "~3.9.3",
|
|
||||||
"vue-cli-plugin-electron-builder": "~2.0.0-rc.6",
|
|
||||||
"vue-jest": "^5.0.0-0",
|
|
||||||
"webpack-node-externals": "^3.0.0"
|
|
||||||
},
|
|
||||||
"gitHooks": {
|
|
||||||
"pre-commit": "lint-staged"
|
|
||||||
},
|
|
||||||
"lint-staged": {
|
|
||||||
"*.{js,jsx,vue,ts,tsx}": [
|
|
||||||
"vue-cli-service lint",
|
|
||||||
"git add"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://gitlab.com/crimata/electron.git",
|
|
||||||
"release": "latest"
|
|
||||||
},
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"packageManager": "yarn@1.22.11"
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
|
||||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
|
||||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<noscript>
|
|
||||||
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
|
||||||
</noscript>
|
|
||||||
<div id="app"></div>
|
|
||||||
<!-- built files will be auto injected -->
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 4 KiB |
41
src/account.js
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
const { ipcMain } = require("electron");
|
||||||
|
|
||||||
|
const store = require("./utils/store");
|
||||||
|
const { postLogin } = require("./api");
|
||||||
|
const { ipcEmit } = require("./utils/emitter");
|
||||||
|
const { launchSession, endSession } = require("./session");
|
||||||
|
|
||||||
|
async function login(_e, email, password)
|
||||||
|
{
|
||||||
|
const res = await postLogin(email, password);
|
||||||
|
if (res)
|
||||||
|
{
|
||||||
|
const account = res.crimataId;
|
||||||
|
store.set("account", account);
|
||||||
|
ipcEmit("set-account", account);
|
||||||
|
launchSession(account);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function logout()
|
||||||
|
{
|
||||||
|
store.delete("account");
|
||||||
|
ipcEmit("set-account", false);
|
||||||
|
endSession();
|
||||||
|
};
|
||||||
|
|
||||||
|
function initAccount()
|
||||||
|
{
|
||||||
|
ipcMain.handle("login", login);
|
||||||
|
ipcMain.on("logout", logout);
|
||||||
|
|
||||||
|
const account = store.get("account");
|
||||||
|
if (account) launchSession(account);
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.initAccount = initAccount;
|
||||||
124
src/account.ts
|
|
@ -1,124 +0,0 @@
|
||||||
import { app } from "electron";
|
|
||||||
|
|
||||||
import { LaunchSession, EndSession } from "@/session";
|
|
||||||
import { store } from "@/composables/useStore";
|
|
||||||
import { postAuth, postLogin, postLogout } from "@/api/account";
|
|
||||||
import { backgroundMitt, ipcEmit } from "@/composables/useEmitter";
|
|
||||||
|
|
||||||
/* Either null or a crimataId */
|
|
||||||
let account: string | null = null;
|
|
||||||
|
|
||||||
const parseAuthRes = (authRes: any) => {
|
|
||||||
const token = authRes.headers['set-cookie'][0].split(";")[0].split("=")[1] as string;
|
|
||||||
const crimataId = authRes.data as string;
|
|
||||||
return {token, crimataId}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const accountAuth = async (): Promise<void> => {
|
|
||||||
|
|
||||||
/* attempt to get a login token from the store */
|
|
||||||
const token = store.get("token");
|
|
||||||
|
|
||||||
try {
|
|
||||||
|
|
||||||
if (token) {
|
|
||||||
|
|
||||||
// attempt to login with token
|
|
||||||
const res = await postAuth(token);
|
|
||||||
const parsed = parseAuthRes(res);
|
|
||||||
|
|
||||||
// save jwt token and profile
|
|
||||||
store.set("token", parsed.token);
|
|
||||||
account = parsed.crimataId
|
|
||||||
|
|
||||||
// launch session
|
|
||||||
LaunchSession(parsed.token);
|
|
||||||
|
|
||||||
} else throw(new Error('Failed to authenticate (no token).'));
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e);
|
|
||||||
store.delete("token");
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
|
|
||||||
// push state changes to the frontend
|
|
||||||
updateAppState();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
export const accountLogin = async (payload: any): Promise<Error | void> => {
|
|
||||||
|
|
||||||
const creds = payload as AccountCredentials;
|
|
||||||
|
|
||||||
try {
|
|
||||||
|
|
||||||
// attempt login with email password
|
|
||||||
const res = await postLogin(creds.email, creds.password);
|
|
||||||
const parsed = parseAuthRes(res);
|
|
||||||
|
|
||||||
// save jwt token and profile
|
|
||||||
store.set("token", parsed.token);
|
|
||||||
account = parsed.crimataId;
|
|
||||||
|
|
||||||
// launch session
|
|
||||||
LaunchSession(parsed.token);
|
|
||||||
|
|
||||||
// push state changes to the frontend
|
|
||||||
updateAppState();
|
|
||||||
|
|
||||||
return;
|
|
||||||
|
|
||||||
} catch(e) {
|
|
||||||
store.delete("token");
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export const accountLogout = async (): Promise<Error | void> => {
|
|
||||||
|
|
||||||
try {
|
|
||||||
// post logout to backend
|
|
||||||
await postLogout();
|
|
||||||
|
|
||||||
// remove key and account
|
|
||||||
store.delete("token");
|
|
||||||
account = null;
|
|
||||||
|
|
||||||
// push account state to browser
|
|
||||||
ipcEmit("set-account", account);
|
|
||||||
|
|
||||||
// kill crimata platform session
|
|
||||||
EndSession();
|
|
||||||
|
|
||||||
return;
|
|
||||||
|
|
||||||
} catch(e) {
|
|
||||||
console.log('[ACCOUNT]', e);
|
|
||||||
return (new Error('Failed to logout. Please try again.'));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// logout when auth fails on platform end
|
|
||||||
backgroundMitt.on("close-auth-fail", async (_payload: any) => {
|
|
||||||
|
|
||||||
await accountLogout();
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
// emits state of account, version, and UI
|
|
||||||
export const updateAppState = () => {
|
|
||||||
|
|
||||||
ipcEmit("set-account", account);
|
|
||||||
|
|
||||||
ipcEmit("set-version", app.getVersion());
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
19
src/api.js
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
const axios = require('axios').default;
|
||||||
|
|
||||||
|
const config = require("./config");
|
||||||
|
|
||||||
|
const baseUrl = config.BUSINESS_URL + config.BUSINESS_PREFIX;
|
||||||
|
|
||||||
|
async function postLogin(email, password)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
const res = await axios.post(baseUrl + "/account/login", {
|
||||||
|
email: email, password: password
|
||||||
|
});
|
||||||
|
return res.data;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.postLogin = postLogin;
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
|
|
||||||
import useHttp from "@/composables/useHttp";
|
|
||||||
import axios from "axios";
|
|
||||||
import {config} from "@/config";
|
|
||||||
|
|
||||||
const { post } = useHttp();
|
|
||||||
|
|
||||||
export const postAuth = async (token: string) => (
|
|
||||||
await axios({
|
|
||||||
url: config.BUSINESS_URL + config.BUSINESS_PREFIX + '/account/authenticate',
|
|
||||||
headers: {
|
|
||||||
Cookie: `crimataCookie=${token}`
|
|
||||||
},
|
|
||||||
method: 'POST',
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
export const postLogin = async (email: string, password: string) => (
|
|
||||||
await post('/account/login', { email, password })
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
export const postLogout =
|
|
||||||
async (): Promise<null | Error> => (await post('/account/logout'));
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
BIN
src/assets/tray/000.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 4 KiB After Width: | Height: | Size: 4 KiB |
BIN
src/assets/tray/001.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
BIN
src/assets/tray/010.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
BIN
src/assets/tray/011.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.4 KiB |
BIN
src/assets/tray/100.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
BIN
src/assets/tray/101.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
BIN
src/assets/tray/110.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 4.6 KiB |
BIN
src/assets/tray/111.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 4.6 KiB |
109
src/audio.js
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
const nodeAudio = require("@crimata/nodeaudio");
|
||||||
|
const { encode, decode } = require("base64-arraybuffer");
|
||||||
|
const { globalShortcut, ipcMain } = require("electron");
|
||||||
|
|
||||||
|
const { sendMessage } = require("./io");
|
||||||
|
const { updateTray } = require("./tray");
|
||||||
|
const { backgroundMitt, ipcEmit } = require("./utils/emitter");
|
||||||
|
|
||||||
|
let inputDevice;
|
||||||
|
let outputDevice;
|
||||||
|
|
||||||
|
let setWriteId;
|
||||||
|
let setStreamsId;
|
||||||
|
|
||||||
|
let playbackId; /* UID of the message being played */
|
||||||
|
|
||||||
|
const streamState = { rec: false, pb: false };
|
||||||
|
|
||||||
|
const /** @type {Int16Array[]} */ chunks = [];
|
||||||
|
|
||||||
|
backgroundMitt.on("data", (int16Arr) => {
|
||||||
|
if (streamState.rec) chunks.push(int16Arr);
|
||||||
|
});
|
||||||
|
|
||||||
|
backgroundMitt.on("write", (int16Arr) => {
|
||||||
|
clearTimeout(setWriteId);
|
||||||
|
setWriteId = setTimeout(() => setPlaybackStatus(false), 500);
|
||||||
|
});
|
||||||
|
|
||||||
|
function setStreams()
|
||||||
|
{
|
||||||
|
const defaultInput = nodeAudio.core.GetDefaultInputDevice();
|
||||||
|
const defaultOutput = nodeAudio.core.GetDefaultOutputDevice();
|
||||||
|
|
||||||
|
if (inputDevice !== defaultInput)
|
||||||
|
{
|
||||||
|
inputDevice = defaultInput;
|
||||||
|
nodeAudio.core.CloseInputStream(inputDevice);
|
||||||
|
nodeAudio.core.OpenInputStream(inputDevice);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (outputDevice !== defaultOutput)
|
||||||
|
{
|
||||||
|
outputDevice = defaultOutput;
|
||||||
|
nodeAudio.core.CloseOutputStream(outputDevice);
|
||||||
|
nodeAudio.core.OpenOutputStream(outputDevice);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopRecording()
|
||||||
|
{
|
||||||
|
setRecordingStatus(false);
|
||||||
|
|
||||||
|
sendMessage({
|
||||||
|
text: false,
|
||||||
|
audio: encode(nodeAudio.utils.mergeChunks(chunks).buffer)
|
||||||
|
});
|
||||||
|
|
||||||
|
chunks.length = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function initAudio()
|
||||||
|
{
|
||||||
|
nodeAudio.core.Initialize(backgroundMitt.emit.bind(backgroundMitt));
|
||||||
|
setStreamsId = setInterval(setStreams, 2000);
|
||||||
|
|
||||||
|
const res = globalShortcut.register('CommandOrControl+Return', () => {
|
||||||
|
streamState.rec ? stopRecording() : setRecordingStatus(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res) throw new Error("Failed to register recording shortcut");
|
||||||
|
}
|
||||||
|
|
||||||
|
function playback(base64String, id)
|
||||||
|
{
|
||||||
|
/* terminate any current playback */
|
||||||
|
nodeAudio.core.CancelPlayback();
|
||||||
|
ipcEmit("playback", playbackId, false);
|
||||||
|
|
||||||
|
playbackId = id;
|
||||||
|
setPlaybackStatus(true);
|
||||||
|
nodeAudio.core.WriteToOutputStream(decode(base64String));
|
||||||
|
}
|
||||||
|
|
||||||
|
function terminateAudio()
|
||||||
|
{
|
||||||
|
globalShortcut.unregisterAll();
|
||||||
|
clearInterval(setStreamsId);
|
||||||
|
nodeAudio.core.Terminate();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setRecordingStatus(status)
|
||||||
|
{
|
||||||
|
streamState.rec = status;
|
||||||
|
ipcEmit("record", status);
|
||||||
|
updateTray("recording", streamState.rec);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPlaybackStatus(status)
|
||||||
|
{
|
||||||
|
streamState.pb = status;
|
||||||
|
ipcEmit("playback", playbackId, status);
|
||||||
|
updateTray("playback", status);
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.initAudio = initAudio;
|
||||||
|
exports.terminateAudio = terminateAudio;
|
||||||
|
exports.playback = playback;
|
||||||
|
exports.streamState = streamState;
|
||||||
124
src/audio.ts
|
|
@ -1,124 +0,0 @@
|
||||||
const FFT = require('fft.js');
|
|
||||||
import { globalShortcut } from "electron";
|
|
||||||
const nodeAudio = require('@crimata/nodeaudio');
|
|
||||||
|
|
||||||
import { sendMessage } from "@/io";
|
|
||||||
import { updateTray } from "@/tray";
|
|
||||||
import { backgroundMitt, ipcEmit } from '@/composables/useEmitter';
|
|
||||||
|
|
||||||
let inputDevice: number;
|
|
||||||
let outputDevice: number;
|
|
||||||
|
|
||||||
let setWriteId: ReturnType<typeof setTimeout>;
|
|
||||||
let setStreamsId: ReturnType<typeof setTimeout>;
|
|
||||||
|
|
||||||
let playbackId: string;
|
|
||||||
|
|
||||||
const streamState = { rec: false, pb: false };
|
|
||||||
|
|
||||||
const chunks: Int16Array[] = [];
|
|
||||||
|
|
||||||
const fft = new FFT(4);
|
|
||||||
|
|
||||||
function calcFFT(int16Arr: Int16Array)
|
|
||||||
{
|
|
||||||
const out = fft.createComplexArray();
|
|
||||||
fft.realTransform(out, int16Arr);
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
backgroundMitt.on("read", (int16Arr: Int16Array) =>
|
|
||||||
{
|
|
||||||
if (streamState.rec)
|
|
||||||
{
|
|
||||||
chunks.push(int16Arr);
|
|
||||||
ipcEmit("recording", calcFFT(int16Arr)); /* fft used for animation */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
backgroundMitt.on("write", (int16Arr: Int16Array) =>
|
|
||||||
{
|
|
||||||
ipcEmit("playback", { uid: playbackId, fft: calcFFT(int16Arr) });
|
|
||||||
streamState.pb = true;
|
|
||||||
|
|
||||||
clearTimeout(setWriteId);
|
|
||||||
setWriteId = setTimeout(() => streamState.pb = false, 1000);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
function setStreams()
|
|
||||||
{
|
|
||||||
const defaultInput = nodeAudio.core.GetDefaultInputDevice();
|
|
||||||
const defaultOutput = nodeAudio.core.GetDefaultOutputDevice();
|
|
||||||
|
|
||||||
if (inputDevice !== defaultInput)
|
|
||||||
{
|
|
||||||
inputDevice = defaultInput;
|
|
||||||
nodeAudio.core.CloseInputStream(inputDevice);
|
|
||||||
nodeAudio.core.OpenInputStream(inputDevice);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (outputDevice !== defaultOutput)
|
|
||||||
{
|
|
||||||
outputDevice = defaultOutput;
|
|
||||||
nodeAudio.core.CloseOutputStream(outputDevice);
|
|
||||||
nodeAudio.core.OpenOutputStream(outputDevice);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function startRecording()
|
|
||||||
{
|
|
||||||
streamState.rec = true;
|
|
||||||
updateTray("recording", streamState.rec);
|
|
||||||
}
|
|
||||||
|
|
||||||
function stopRecording()
|
|
||||||
{
|
|
||||||
streamState.rec = false;
|
|
||||||
updateTray("recording", streamState.rec);
|
|
||||||
|
|
||||||
/* Send the data to the platform right away */
|
|
||||||
sendMessage({
|
|
||||||
text: false,
|
|
||||||
audio: nodeAudio.utils.mergeChunks(chunks)
|
|
||||||
} as Raw);
|
|
||||||
|
|
||||||
chunks.length = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
function InitAudio()
|
|
||||||
{
|
|
||||||
nodeAudio.core.Initialize(backgroundMitt.emit.bind(backgroundMitt));
|
|
||||||
setStreamsId = setInterval(setStreams, 2000); // sense device
|
|
||||||
|
|
||||||
/* Toggle recording switch with global shortcut */
|
|
||||||
globalShortcut.register('CommandOrControl+R', () =>
|
|
||||||
{
|
|
||||||
if (streamState.rec)
|
|
||||||
{
|
|
||||||
stopRecording();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
startRecording();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Will override an existing playback process */
|
|
||||||
function playback(audio: Int16Array, uid: string)
|
|
||||||
{
|
|
||||||
playbackId = uid; /* uid of message */
|
|
||||||
// nodeAudio.core.CancelPlayback(); /* terminate any current playback */
|
|
||||||
nodeAudio.core.WriteToOutputStream(audio.buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
function TerminateAudio()
|
|
||||||
{
|
|
||||||
clearInterval(setStreamsId);
|
|
||||||
nodeAudio.core.Terminate();
|
|
||||||
}
|
|
||||||
|
|
||||||
export { InitAudio, TerminateAudio, playback, streamState };
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
const { autoUpdater } = require('electron-updater');
|
|
||||||
|
|
||||||
let win: boolean;
|
|
||||||
|
|
||||||
// Listen for window creation.
|
|
||||||
backgroundMitt.on('window-active', (state: boolean) => {
|
|
||||||
win = state;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Auto updating.
|
|
||||||
autoUpdater.requestHeaders = { 'PRIVATE-TOKEN': 'mvvgWYwWnot4bisiQMh_' }
|
|
||||||
|
|
||||||
autoUpdater.on('update-available', (info: any) => {
|
|
||||||
console.log(`Update available: ${info.version}`)
|
|
||||||
})
|
|
||||||
|
|
||||||
autoUpdater.on('update-downloaded', (info: any) => {
|
|
||||||
|
|
||||||
const updateDialog = {
|
|
||||||
type: 'info',
|
|
||||||
buttons: ['Restart', 'Later'],
|
|
||||||
title: 'Application Update',
|
|
||||||
message: info.version,
|
|
||||||
detail: 'A new version has been downloaded. Restart the application to apply the updates.'
|
|
||||||
}
|
|
||||||
|
|
||||||
dialog.showMessageBox(updateDialog).then((returnValue) => {
|
|
||||||
if (returnValue.response === 0) autoUpdater.quitAndInstall()
|
|
||||||
})
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
@ -1,51 +0,0 @@
|
||||||
|
|
||||||
import axios, { AxiosRequestConfig } from 'axios';
|
|
||||||
import {config} from "@/config";
|
|
||||||
|
|
||||||
const baseURL = config.BUSINESS_URL + config.BUSINESS_PREFIX;
|
|
||||||
|
|
||||||
interface Request {
|
|
||||||
endpoint: string;
|
|
||||||
query?: Record<string, any>;
|
|
||||||
config?: Record<string, any>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const makeQuery = (reqQuery: Record<string, any>) => {
|
|
||||||
|
|
||||||
let result = '';
|
|
||||||
|
|
||||||
result = '?' + Object.entries(reqQuery)
|
|
||||||
.map(([ key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
|
|
||||||
.join('&')
|
|
||||||
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
export default function useHttp() {
|
|
||||||
|
|
||||||
const api = axios.create({
|
|
||||||
baseURL,
|
|
||||||
withCredentials: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
const post = async (endpoint: string, payload?: Record<string, any>): Promise<any> => (
|
|
||||||
await api.post(endpoint, payload)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
const get = async (req: Request) => {
|
|
||||||
|
|
||||||
if (req.query) {
|
|
||||||
req.endpoint += makeQuery(req.query);
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await api.get(req.endpoint, req.config);
|
|
||||||
return res;
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
get, post
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,80 +0,0 @@
|
||||||
|
|
||||||
import { ipcMain, IpcMainInvokeEvent, IpcMainEvent } from "electron";
|
|
||||||
|
|
||||||
export class IpcHandler<InputParam, ReturnType> implements IIpcHandler<InputParam, ReturnType> {
|
|
||||||
|
|
||||||
readonly channel: string;
|
|
||||||
|
|
||||||
readonly _handlerCallback: IpcHandlerCallback<InputParam, ReturnType>;
|
|
||||||
|
|
||||||
constructor(options: {
|
|
||||||
channel: string;
|
|
||||||
handlerCallback: IpcHandlerCallback<InputParam, ReturnType>;
|
|
||||||
}) {
|
|
||||||
this.channel = options.channel;
|
|
||||||
this._handlerCallback = options.handlerCallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
handle() {
|
|
||||||
this.remove();
|
|
||||||
ipcMain.handle(this.channel, this._onInvoke);
|
|
||||||
}
|
|
||||||
|
|
||||||
remove() {
|
|
||||||
ipcMain.removeHandler(this.channel);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _onInvoke = (_e: IpcMainInvokeEvent, payload?: string | null): Promise<Error | ReturnType> => {
|
|
||||||
|
|
||||||
return new Promise(async (resolve, reject) => {
|
|
||||||
|
|
||||||
console.log(`[IPC] Handle:${this.channel}`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
|
|
||||||
const params = payload ? JSON.parse(payload) : null;
|
|
||||||
|
|
||||||
const res = await this._handlerCallback(params);
|
|
||||||
|
|
||||||
resolve(res as unknown as ReturnType);
|
|
||||||
|
|
||||||
} catch(e) {
|
|
||||||
console.log(`[IPC] Error:${this.channel}`);
|
|
||||||
reject(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export class IpcListener<InputParam> implements IIpcListener<InputParam> {
|
|
||||||
|
|
||||||
readonly channel: string;
|
|
||||||
|
|
||||||
readonly _listenerCallback: IpcListenerCallback<InputParam>;
|
|
||||||
|
|
||||||
constructor(options: {
|
|
||||||
channel: string;
|
|
||||||
listenerCallback: IpcListenerCallback<InputParam>;
|
|
||||||
}) {
|
|
||||||
this.channel = options.channel;
|
|
||||||
this._listenerCallback = options.listenerCallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
listen() {
|
|
||||||
this.remove();
|
|
||||||
ipcMain.on(this.channel, this._onPost);
|
|
||||||
}
|
|
||||||
|
|
||||||
remove() {
|
|
||||||
ipcMain.removeAllListeners(this.channel);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _onPost = (_e: IpcMainEvent, payload: InputParam): void => {
|
|
||||||
|
|
||||||
console.log(`[IPC] Post: ${this.channel}`);
|
|
||||||
this._listenerCallback(payload);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
const Store = require('electron-store');
|
|
||||||
|
|
||||||
const schema = {
|
|
||||||
token: {
|
|
||||||
type: 'string',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const store = new Store({
|
|
||||||
schema,
|
|
||||||
encryptionKey: "super user test"
|
|
||||||
});
|
|
||||||
|
|
@ -1,99 +0,0 @@
|
||||||
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
import WebSocket from 'ws';
|
|
||||||
|
|
||||||
|
|
||||||
const _connectionCheckTimeout = 4000;
|
|
||||||
const _reconnectTimeout = 1000;
|
|
||||||
let _connectionCheckInterval: ReturnType<typeof setTimeout>;
|
|
||||||
|
|
||||||
/* ws-status key:
|
|
||||||
* 0 - onMessage (connected)
|
|
||||||
* 1 - pongMessage (nominal)
|
|
||||||
* 2 - closeMessage (reconnecting)
|
|
||||||
* 3 - pingErrorMessage (connection lost)
|
|
||||||
*/
|
|
||||||
|
|
||||||
export default function useWebSockets(emit: (eventName: string | symbol, [...args]: any) => boolean)
|
|
||||||
{
|
|
||||||
|
|
||||||
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}));
|
|
||||||
|
|
||||||
emit("ws-status", 0);
|
|
||||||
|
|
||||||
// ping server
|
|
||||||
_connectionCheckInterval = setInterval(() => {
|
|
||||||
|
|
||||||
socket.ping(null, true, (e: Error) => {
|
|
||||||
if (e) {
|
|
||||||
socket.close();
|
|
||||||
emit("ws-status", 3);
|
|
||||||
setTimeout(() => connect(socketUrl, secret), _reconnectTimeout);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
}, _connectionCheckTimeout);
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("message", (event: WebSocket.MessageEvent) => {
|
|
||||||
console.log("message received", event);
|
|
||||||
emit("ws-message", event.toString());
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("close", (code: number, reason: string) => {
|
|
||||||
emit("ws-status", 2);
|
|
||||||
clearInterval(_connectionCheckInterval);
|
|
||||||
if (code !== 1000 || reason !== 'session-logout') {
|
|
||||||
setTimeout(() => {
|
|
||||||
connect(socketUrl, secret), _reconnectTimeout
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("pong", () => emit("ws-status", 1));
|
|
||||||
|
|
||||||
socket.on('error', () => {});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
const close = (code: number, reason: string) => {
|
|
||||||
if (socket) {
|
|
||||||
socket.close(code, reason);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
connect,
|
|
||||||
send,
|
|
||||||
close
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
|
const { app } = require("electron");
|
||||||
import { app } from "electron";
|
|
||||||
|
|
||||||
const env = process.env;
|
const env = process.env;
|
||||||
|
|
||||||
|
|
@ -9,9 +8,9 @@ const PLATFORM_IP = env.PLATFORM_IP || 'http://127.0.0.1';
|
||||||
const BUSINESS_PORT = env.BUSINESS_PORT || 3000;
|
const BUSINESS_PORT = env.BUSINESS_PORT || 3000;
|
||||||
const BUSINESS_IP = env.BUSINESS_IP || 'http://127.0.0.1';
|
const BUSINESS_IP = env.BUSINESS_IP || 'http://127.0.0.1';
|
||||||
|
|
||||||
export const config = {
|
module.exports = {
|
||||||
PLATFORM_URL: `${PLATFORM_IP}:${PLATFORM_PORT}`,
|
PLATFORM_URL: `${PLATFORM_IP}:${PLATFORM_PORT}`,
|
||||||
BUSINESS_URL: `${BUSINESS_IP}:${BUSINESS_PORT}`,
|
BUSINESS_URL: `${BUSINESS_IP}:${BUSINESS_PORT}`,
|
||||||
BUSINESS_PREFIX: '/api',
|
BUSINESS_PREFIX: '/api',
|
||||||
configPath: app.getPath('userData'),
|
configPath: app.getPath('userData'),
|
||||||
}
|
}
|
||||||
53
src/init.ts
|
|
@ -1,53 +0,0 @@
|
||||||
/**
|
|
||||||
* Entry point for Crimata electron app.
|
|
||||||
* "Look on my Works, ye Mighty, and despair!"
|
|
||||||
*/
|
|
||||||
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
import { app, protocol, globalShortcut } from "electron";
|
|
||||||
|
|
||||||
import main from "@/main";
|
|
||||||
import { CreateWindow, winState } from "@/window";
|
|
||||||
import { TerminateAudio } from '@/audio';
|
|
||||||
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');
|
|
||||||
|
|
||||||
/* Start main process on ready */
|
|
||||||
app.on("ready", async () =>
|
|
||||||
{
|
|
||||||
await main();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
app.on("will-quit", () =>
|
|
||||||
{
|
|
||||||
TerminateAudio();
|
|
||||||
globalShortcut.unregisterAll();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// When user clicks app icon (re-open)
|
|
||||||
app.on("activate", () =>
|
|
||||||
{
|
|
||||||
if (!winState.open) CreateWindow();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Exit cleanly on request from parent process in development mode.
|
|
||||||
if (isDev)
|
|
||||||
{
|
|
||||||
process.on("SIGTERM", () =>
|
|
||||||
{
|
|
||||||
app.quit();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
58
src/io.js
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
const WebSocket = require("ws");
|
||||||
|
|
||||||
|
const config = require("./config");
|
||||||
|
const { ipcMain } = require("electron");
|
||||||
|
const { updateTray } = require("./tray");
|
||||||
|
const { backgroundMitt, ipcEmit } = require("./utils/emitter");
|
||||||
|
|
||||||
|
let connection = null;
|
||||||
|
|
||||||
|
let pingId;
|
||||||
|
let reconnectId;
|
||||||
|
|
||||||
|
function connectToPlatform(account)
|
||||||
|
{
|
||||||
|
if (pingId) clearInterval(pingId);
|
||||||
|
|
||||||
|
connection = new WebSocket(`${config.PLATFORM_URL}/${account}`)
|
||||||
|
|
||||||
|
.on("open", () => pingId = setInterval(() => connection.ping(null, true), 1000))
|
||||||
|
|
||||||
|
.on("pong", () => setConnectionStatus(0))
|
||||||
|
|
||||||
|
.on("error", () => {}) /** keep silent on error */
|
||||||
|
|
||||||
|
.on("message", (payload) => backgroundMitt.emit("ws-message", JSON.parse(payload)))
|
||||||
|
|
||||||
|
.on("close", (code, reason) => {
|
||||||
|
setConnectionStatus(1);
|
||||||
|
if (reason !== "logout") {
|
||||||
|
reconnectId = setTimeout(() => connectToPlatform(account), 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendMessage(message)
|
||||||
|
{
|
||||||
|
if (connection.readyState === WebSocket.OPEN) {
|
||||||
|
connection.send(JSON.stringify(message));
|
||||||
|
} else console.log("Failed to send message");
|
||||||
|
}
|
||||||
|
|
||||||
|
function disconnectFromPlatform()
|
||||||
|
{
|
||||||
|
clearTimeout(reconnectId);
|
||||||
|
connection.close(1000, "logout");
|
||||||
|
}
|
||||||
|
|
||||||
|
function setConnectionStatus(status)
|
||||||
|
{
|
||||||
|
updateTray("disconnect", status);
|
||||||
|
ipcEmit("ws-status", status);
|
||||||
|
}
|
||||||
|
|
||||||
|
ipcMain.on("message", (_e, message) => sendMessage(message));
|
||||||
|
|
||||||
|
exports.connectToPlatform = connectToPlatform;
|
||||||
|
exports.sendMessage = sendMessage;
|
||||||
|
exports.disconnectFromPlatform = disconnectFromPlatform;
|
||||||
34
src/io.ts
|
|
@ -1,34 +0,0 @@
|
||||||
import { config } from "@/config";
|
|
||||||
import { updateTray } from "@/tray";
|
|
||||||
import useWebsockets from "@/composables/useWebsockets";
|
|
||||||
import { backgroundMitt, ipcEmit } from "@/composables/useEmitter";
|
|
||||||
|
|
||||||
backgroundMitt.on("ws-status", (status: any) =>
|
|
||||||
{
|
|
||||||
let simpleStatus = false;
|
|
||||||
if (status == 2 || status == 3) simpleStatus = true;
|
|
||||||
|
|
||||||
updateTray("disconnect", simpleStatus);
|
|
||||||
|
|
||||||
ipcEmit('set-connection-status', status);
|
|
||||||
});
|
|
||||||
|
|
||||||
const { connect, send, close } =
|
|
||||||
useWebsockets(backgroundMitt.emit.bind(backgroundMitt))
|
|
||||||
|
|
||||||
function sendMessage(message: Raw | Request): void
|
|
||||||
{
|
|
||||||
send(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ConnectToPlatform(platformKey: string)
|
|
||||||
{
|
|
||||||
connect(config.PLATFORM_URL, platformKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
function DisconnectFromPlatform(code: number, reason: string)
|
|
||||||
{
|
|
||||||
close(code, reason);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { ConnectToPlatform, DisconnectFromPlatform, sendMessage };
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
import { accountLogin, accountLogout } from "@/account";
|
|
||||||
import { IpcHandler } from "@/composables/useIpcMain";
|
|
||||||
|
|
||||||
|
|
||||||
const LOGIN_CHANNEL = "invoke-account-login";
|
|
||||||
const LOGOUT_CHANNEL = "invoke-account-logout";
|
|
||||||
|
|
||||||
export const loginHandler = new IpcHandler({
|
|
||||||
channel: LOGIN_CHANNEL,
|
|
||||||
handlerCallback: accountLogin
|
|
||||||
});
|
|
||||||
|
|
||||||
export const logoutHandler = new IpcHandler({
|
|
||||||
channel: LOGOUT_CHANNEL,
|
|
||||||
handlerCallback: accountLogout
|
|
||||||
});
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
import * as handlers from "./handlers";
|
|
||||||
import * as listeners from "./listeners";
|
|
||||||
|
|
||||||
const ipcHandlers: IPCHandlers = {};
|
|
||||||
const ipcListeners: IPCListeners = {};
|
|
||||||
|
|
||||||
const _initHandlers = (): void => {
|
|
||||||
for (const [key, handler] of Object.entries(handlers)) {
|
|
||||||
if (!(key in ipcHandlers)) {
|
|
||||||
ipcHandlers[key] = handler;
|
|
||||||
handler.handle();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const _initListeners = (): void => {
|
|
||||||
for (const [key, listener] of Object.entries(listeners)) {
|
|
||||||
if (!(key in ipcListeners)) {
|
|
||||||
ipcListeners[key] = listener;
|
|
||||||
listener.listen();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function initIpcMain(): void {
|
|
||||||
_initHandlers();
|
|
||||||
_initListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
import { sendMessage } from '@/io';
|
|
||||||
import { playback } from "@/audio";
|
|
||||||
import { winState, onNavBar } from "@/window";
|
|
||||||
import { updateAppState } from "@/account";
|
|
||||||
import { IpcListener } from "@/composables/useIpcMain"
|
|
||||||
|
|
||||||
const CLIENT_MESSAGE_CHANNEL = "post-session-send"
|
|
||||||
const APP_MOUNT_CHANNEL = "post-app-mount";
|
|
||||||
const NAV_BAR_CHANNEL = "post-nav-bar";
|
|
||||||
const POST_WINDOW_FOCUS = 'post-window-focus';
|
|
||||||
const PLAYBACK_CHANNEL = "post-playback;"
|
|
||||||
|
|
||||||
export const messageListener = new IpcListener<Raw | Request>({
|
|
||||||
channel: CLIENT_MESSAGE_CHANNEL,
|
|
||||||
listenerCallback: sendMessage
|
|
||||||
});
|
|
||||||
|
|
||||||
export const appMountListener = new IpcListener<null>({
|
|
||||||
channel: APP_MOUNT_CHANNEL,
|
|
||||||
listenerCallback: updateAppState
|
|
||||||
});
|
|
||||||
|
|
||||||
export const navBarListener = new IpcListener({
|
|
||||||
channel: NAV_BAR_CHANNEL,
|
|
||||||
listenerCallback: onNavBar
|
|
||||||
});
|
|
||||||
|
|
||||||
export const windowFocusListener = new IpcListener<boolean>({
|
|
||||||
channel: POST_WINDOW_FOCUS,
|
|
||||||
listenerCallback: (state) => winState.focus = state
|
|
||||||
});
|
|
||||||
|
|
||||||
// export const playbackListener = new IpcListener({
|
|
||||||
// channel: PLAYBACK_CHANNEL,
|
|
||||||
// listenerCallback: playback
|
|
||||||
// });
|
|
||||||
26
src/main.js
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
const { app, protocol, ipcMain } = require("electron");
|
||||||
|
|
||||||
|
const store = require("./utils/store");
|
||||||
|
|
||||||
|
const { initTray } = require("./tray");
|
||||||
|
const { postLogin } = require("./api");
|
||||||
|
const { createWin } = require("./window");
|
||||||
|
const { initAccount } = require("./account");
|
||||||
|
const { terminateAudio } = require("./audio");
|
||||||
|
|
||||||
|
app.on("ready", () =>
|
||||||
|
{
|
||||||
|
initTray();
|
||||||
|
|
||||||
|
createWin();
|
||||||
|
|
||||||
|
initAccount();
|
||||||
|
});
|
||||||
|
|
||||||
|
app.on("will-quit", terminateAudio);
|
||||||
|
|
||||||
|
// When user clicks app icon (re-open)
|
||||||
|
app.on("activate", createWin);
|
||||||
|
|
||||||
|
// Prevents app from quitting on window close event
|
||||||
|
app.on('window-all-closed', (e) => e.preventDefault());
|
||||||
20
src/main.ts
|
|
@ -1,20 +0,0 @@
|
||||||
import { initTray } from '@/tray';
|
|
||||||
import { CreateWindow } from "@/window";
|
|
||||||
import initIpcMain from "@/ipc/index";
|
|
||||||
import { accountAuth } from "@/account";
|
|
||||||
|
|
||||||
export default async function main() {
|
|
||||||
|
|
||||||
/* initiate render process event listeners & handlers */
|
|
||||||
initIpcMain();
|
|
||||||
|
|
||||||
/* initiate tray icon in default state */
|
|
||||||
initTray();
|
|
||||||
|
|
||||||
/* launch browser window */
|
|
||||||
await CreateWindow();
|
|
||||||
|
|
||||||
/* try to authenticate with token */
|
|
||||||
accountAuth();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,74 +0,0 @@
|
||||||
<template>
|
|
||||||
<!-- Only render when profile has been set -->
|
|
||||||
<main id="app" v-if="ready">
|
|
||||||
|
|
||||||
<Header />
|
|
||||||
|
|
||||||
<Messenger v-if="account"/>
|
|
||||||
<Login v-else />
|
|
||||||
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<!-- Show spash screen if ready is false -->
|
|
||||||
<Splash v-else />
|
|
||||||
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
|
|
||||||
import { defineComponent, onMounted } from "vue";
|
|
||||||
|
|
||||||
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 { ready, account, setAccount, clearAccount } from "@/render/shared/account";
|
|
||||||
import { postAppMount } from "@/render/ipc";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
|
|
||||||
components: {
|
|
||||||
Splash,
|
|
||||||
Messenger,
|
|
||||||
Login,
|
|
||||||
Header
|
|
||||||
},
|
|
||||||
|
|
||||||
setup() {
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
|
|
||||||
postAppMount()
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
account,
|
|
||||||
ready
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
</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>
|
|
||||||
11
src/render/assets/mailAvatar.svg
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="12" viewBox="0 0 16 12">
|
||||||
|
<g id="Group_618" data-name="Group 618" transform="translate(-777 -3651)">
|
||||||
|
<g id="Rectangle_1621" data-name="Rectangle 1621" transform="translate(777 3651)" fill="none" stroke="#58c4fd" stroke-width="2.3">
|
||||||
|
<rect width="16" height="12" rx="4" stroke="none"/>
|
||||||
|
<rect x="1.15" y="1.15" width="13.7" height="9.7" rx="2.85" fill="none"/>
|
||||||
|
</g>
|
||||||
|
<g id="Group_617" data-name="Group 617">
|
||||||
|
<path id="Polygon_6" data-name="Polygon 6" d="M6.419.415a1,1,0,0,1,1.162,0l3.88,2.771A1,1,0,0,1,10.88,5H3.12a1,1,0,0,1-.581-1.814Z" transform="translate(792 3656) rotate(180)" fill="#58c4fd"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 710 B |
|
|
@ -1,81 +0,0 @@
|
||||||
class AudioVisualizer {
|
|
||||||
constructor( audioContext, processFrame, processError ) {
|
|
||||||
this.audioContext = audioContext;
|
|
||||||
this.processFrame = processFrame;
|
|
||||||
this.connectStream = this.connectStream.bind( this );
|
|
||||||
navigator.mediaDevices.getUserMedia( { audio: true, video: false } )
|
|
||||||
.then( this.connectStream )
|
|
||||||
.catch( ( error ) => {
|
|
||||||
if ( processError ) {
|
|
||||||
processError( error );
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
}
|
|
||||||
|
|
||||||
connectStream( stream ) {
|
|
||||||
this.analyser = this.audioContext.createAnalyser();
|
|
||||||
const source = this.audioContext.createMediaStreamSource( stream );
|
|
||||||
source.connect( this.analyser );
|
|
||||||
this.analyser.smoothingTimeConstant = 0.5;
|
|
||||||
this.analyser.fftSize = 32;
|
|
||||||
|
|
||||||
this.initRenderLoop( this.analyser );
|
|
||||||
}
|
|
||||||
|
|
||||||
initRenderLoop() {
|
|
||||||
const frequencyData = new Uint8Array( this.analyser.frequencyBinCount );
|
|
||||||
const processFrame = this.processFrame || ( () => {} );
|
|
||||||
|
|
||||||
const renderFrame = () => {
|
|
||||||
this.analyser.getByteFrequencyData( frequencyData );
|
|
||||||
processFrame( frequencyData );
|
|
||||||
|
|
||||||
requestAnimationFrame( renderFrame );
|
|
||||||
};
|
|
||||||
requestAnimationFrame( renderFrame );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const visualMainElement = document.querySelector( 'main' );
|
|
||||||
const visualValueCount = 16;
|
|
||||||
let visualElements;
|
|
||||||
const createDOMElements = () => {
|
|
||||||
let i;
|
|
||||||
for ( i = 0; i < visualValueCount; ++i ) {
|
|
||||||
const elm = document.createElement( 'div' );
|
|
||||||
visualMainElement.appendChild( elm );
|
|
||||||
}
|
|
||||||
|
|
||||||
visualElements = document.querySelectorAll( 'main div' );
|
|
||||||
};
|
|
||||||
createDOMElements();
|
|
||||||
|
|
||||||
const init = () => {
|
|
||||||
// Creating initial DOM elements
|
|
||||||
const audioContext = new AudioContext();
|
|
||||||
const initDOM = () => {
|
|
||||||
visualMainElement.innerHTML = '';
|
|
||||||
createDOMElements();
|
|
||||||
};
|
|
||||||
initDOM();
|
|
||||||
|
|
||||||
// Swapping values around for a better visual effect
|
|
||||||
const dataMap = { 0: 15, 1: 10, 2: 8, 3: 9, 4: 6, 5: 5, 6: 2, 7: 1, 8: 0, 9: 4, 10: 3, 11: 7, 12: 11, 13: 12, 14: 13, 15: 14 };
|
|
||||||
const processFrame = ( data ) => {
|
|
||||||
const values = Object.values( data );
|
|
||||||
let i;
|
|
||||||
for ( i = 0; i < visualValueCount; ++i ) {
|
|
||||||
const value = values[ dataMap[ i ] ] / 255;
|
|
||||||
const elmStyles = visualElements[ i ].style;
|
|
||||||
elmStyles.transform = `scaleY( ${ value } )`;
|
|
||||||
elmStyles.opacity = Math.max( .25, value );
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const processError = () => {
|
|
||||||
visualMainElement.classList.add( 'error' );
|
|
||||||
visualMainElement.innerText = 'Please allow access to your microphone in order to see this demo.\nNothing bad is going to happen... hopefully :P';
|
|
||||||
}
|
|
||||||
|
|
||||||
const a = new AudioVisualizer( audioContext, processFrame, processError );
|
|
||||||
};
|
|
||||||
44
src/render/components/app.js
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
import Splash from "./splash.js";
|
||||||
|
import Messenger from "./messenger.js";
|
||||||
|
import Login from "./login.js";
|
||||||
|
import Header from "./header.js";
|
||||||
|
|
||||||
|
const account = Vue.ref(null);
|
||||||
|
|
||||||
|
const App =
|
||||||
|
{
|
||||||
|
components: {
|
||||||
|
Splash,
|
||||||
|
Messenger,
|
||||||
|
Login,
|
||||||
|
Header
|
||||||
|
},
|
||||||
|
|
||||||
|
setup()
|
||||||
|
{
|
||||||
|
return { account };
|
||||||
|
},
|
||||||
|
|
||||||
|
template: `
|
||||||
|
<main id="app" v-if="account !== null">
|
||||||
|
|
||||||
|
<Header />
|
||||||
|
|
||||||
|
<Messenger v-if="account"/>
|
||||||
|
<Login v-else />
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<Splash v-else />`
|
||||||
|
}
|
||||||
|
|
||||||
|
window.mainApi.on("set-account", (account_) => {
|
||||||
|
console.log("setting-account");
|
||||||
|
account.value = account_;
|
||||||
|
});
|
||||||
|
|
||||||
|
export default App;
|
||||||
|
export { account };
|
||||||
|
|
||||||
|
// Is called when window is about to close or reload
|
||||||
|
window.onbeforeunload = () => console.log("beforeUnload");
|
||||||
16
src/render/components/bubble.js
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
const Bubble =
|
||||||
|
{
|
||||||
|
props: ["modifier", "text", "child"],
|
||||||
|
|
||||||
|
template: `
|
||||||
|
<div :class="'bubble ' + modifier + '-bubble ' + modifier +'-'+ child">
|
||||||
|
{{ text }}
|
||||||
|
</div> `
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Bubble;
|
||||||
|
|
||||||
|
|
||||||
|
// <div v-show="!seen" class="bubble-notify"></div>
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,98 +0,0 @@
|
||||||
<template>
|
|
||||||
<div :class="`bubble ${modifier}-bubble ${modifier}-${child}`">
|
|
||||||
<div v-show="!seen" class="notify"></div>
|
|
||||||
{{ text }}
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang='ts'>
|
|
||||||
|
|
||||||
import { defineComponent } from 'vue';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: "Bubble",
|
|
||||||
|
|
||||||
props: ["modifier", "text", "child", "seen"],
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
|
|
||||||
.bubble {
|
|
||||||
position: relative;
|
|
||||||
max-width: 66vw;
|
|
||||||
font-family: "SF Pro Text";
|
|
||||||
font-size: 14px;
|
|
||||||
padding: 10px;
|
|
||||||
border-radius: 18px;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ai-bubble {
|
|
||||||
background-color: #FFFFFF;
|
|
||||||
margin-left: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.client-bubble {
|
|
||||||
color: white;
|
|
||||||
background-color: #58C4FD;
|
|
||||||
margin-right: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ai-first-child {
|
|
||||||
border-bottom-left-radius: 9px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ai-middle-child {
|
|
||||||
border-top-left-radius: 9px;
|
|
||||||
border-bottom-left-radius: 9px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ai-last-child {
|
|
||||||
border-top-left-radius: 9px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.client-first-child {
|
|
||||||
border-bottom-right-radius: 9px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.client-middle-child {
|
|
||||||
border-top-right-radius: 9px;
|
|
||||||
border-bottom-right-radius: 9px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.client-last-child {
|
|
||||||
border-top-right-radius: 9px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ai-none-child, .client-none-child {
|
|
||||||
border-top-right-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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,103 +0,0 @@
|
||||||
<template>
|
|
||||||
<div id="container">
|
|
||||||
<img id="logo" src="@/render/assets/connectLogo.svg">
|
|
||||||
<div id="connect">
|
|
||||||
<div class="dot"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
|
|
||||||
import { defineComponent, onMounted, watch } from 'vue'
|
|
||||||
import { hiddenState, useAnim, status, show, hide } from "@/render/shared/connectionStatus";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: "ConnectionStatus",
|
|
||||||
|
|
||||||
setup() {
|
|
||||||
|
|
||||||
watch(status, (val, _oldval) => {
|
|
||||||
if (val === "Connected" || val === "Nominal") {
|
|
||||||
if (!hiddenState) hide();
|
|
||||||
} else {
|
|
||||||
if (hiddenState)
|
|
||||||
show();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
onMounted(useAnim);
|
|
||||||
|
|
||||||
return {
|
|
||||||
status
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
|
|
||||||
#container {
|
|
||||||
position: fixed;
|
|
||||||
width: 100vw;
|
|
||||||
height: 54px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
#connect {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
shape {
|
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background-color: #48E065;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dot {
|
|
||||||
@extend shape;
|
|
||||||
position: relative;
|
|
||||||
transform: translateX(-15px);
|
|
||||||
animation: flashing 1s infinite linear alternate;
|
|
||||||
animation-delay: .25s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dot::before, .dot::after {
|
|
||||||
content: '';
|
|
||||||
display: inline-block;
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dot::before {
|
|
||||||
@extend shape;
|
|
||||||
left: -9px;
|
|
||||||
animation: flashing 1s infinite alternate;
|
|
||||||
animation-delay: 0s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dot::after {
|
|
||||||
@extend shape;
|
|
||||||
left: 9px;
|
|
||||||
animation: flashing 1s infinite alternate;
|
|
||||||
animation-delay: 0.5s;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes flashing {
|
|
||||||
0% {
|
|
||||||
background-color: #48E065;
|
|
||||||
}
|
|
||||||
50%,
|
|
||||||
100% {
|
|
||||||
background-color: #9B9B9B;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
||||||
38
src/render/components/context.js
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
|
||||||
|
const Context =
|
||||||
|
{
|
||||||
|
props: ["modifier", "context", "avatar", "audio", "id"],
|
||||||
|
|
||||||
|
setup(props) {
|
||||||
|
const playback = Vue.ref(false);
|
||||||
|
|
||||||
|
Vue.onMounted(() => {
|
||||||
|
window.mainApi.on("playback", (id, status) => {
|
||||||
|
if (id === props.id) {
|
||||||
|
playback.value = status;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return { playback };
|
||||||
|
},
|
||||||
|
|
||||||
|
template: `
|
||||||
|
<span :class="'context ' + modifier + '-context'">
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="avatar != 'False'"
|
||||||
|
class="context-avatar"
|
||||||
|
:class="{ audio: playback }"
|
||||||
|
>
|
||||||
|
|
||||||
|
<img :src="avatar">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{ context }}
|
||||||
|
|
||||||
|
</span> `
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Context;
|
||||||
|
|
@ -1,83 +0,0 @@
|
||||||
<template>
|
|
||||||
|
|
||||||
<span :class="`context ${modifier}-context`">
|
|
||||||
|
|
||||||
<div v-if="context.img" class="avatar" @click="clickMethod">
|
|
||||||
<img v-click="" v-if="context.img === 'crimata'" :src="require('@/render/assets/crimataAvatar.svg')">
|
|
||||||
<div v-else>{{ context.img }}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{ context.text == false ? " " : context.text }}
|
|
||||||
|
|
||||||
</span>
|
|
||||||
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang='ts'>
|
|
||||||
|
|
||||||
import { defineComponent } from 'vue';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: "Context",
|
|
||||||
|
|
||||||
props: ["modifier", "context", "uid"],
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
|
|
||||||
.context {
|
|
||||||
position: relative;
|
|
||||||
min-height: 14px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
font-family: "SF Compact Display";
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: bold;
|
|
||||||
margin-top: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ai-context {
|
|
||||||
margin-left: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.client-context {
|
|
||||||
margin-right: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
background-color: white;
|
|
||||||
border-radius: 15px;
|
|
||||||
margin-right: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.playing {
|
|
||||||
animation-name: message-playback-anim;
|
|
||||||
animation-duration: 2s;
|
|
||||||
animation-iteration-count: infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes message-playback-anim {
|
|
||||||
0%,
|
|
||||||
100% {
|
|
||||||
box-shadow: 0 0 0 0.4em rgba(195, 195, 195, 0.4), 0 0 0 0.25em rgba(195, 195, 195, 0.15);
|
|
||||||
}
|
|
||||||
25% {
|
|
||||||
box-shadow: 0 0 0 0.15em rgba(195, 195, 195, 0.15), 0 0 0 0.4em rgba(195, 195, 195, 0.3);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
box-shadow: 0 0 0 0.55em rgba(195, 195, 195, 0.55), 0 0 0 0.15em rgba(195, 195, 195, 0.05);
|
|
||||||
}
|
|
||||||
75% {
|
|
||||||
box-shadow: 0 0 0 0.25em rgba(195, 195, 195, 0.25), 0 0 0 0.55em rgba(195, 195, 195, 0.45);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
||||||
199
src/render/components/control/draggify.js
Normal file
|
|
@ -0,0 +1,199 @@
|
||||||
|
const saveLocation = "input_item_position";
|
||||||
|
const defaultPosition = { x: 15, y: window.innerHeight - 300 };
|
||||||
|
|
||||||
|
//---Dragabble Helper Funcs--------------------------------------
|
||||||
|
|
||||||
|
// Calculate distance to nearest side.
|
||||||
|
function calcSideProximity (elementX, elementLength, winW) {
|
||||||
|
|
||||||
|
// Calc right short.
|
||||||
|
let short = winW - elementX - elementLength;
|
||||||
|
|
||||||
|
// See if it's left short.
|
||||||
|
if (elementX + 20 < winW / 2) {
|
||||||
|
short = elementX
|
||||||
|
}
|
||||||
|
|
||||||
|
return short
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update elementX or elementY value on window resize
|
||||||
|
function calcPosition (elementPosition, elementLength, percent, short, win) {
|
||||||
|
|
||||||
|
// Is it close to the right/bottom side?
|
||||||
|
if (percent > 0.75) {
|
||||||
|
elementPosition = win - short - elementLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is it not close to a side?
|
||||||
|
if (percent < 0.75 && percent > 0.25) {
|
||||||
|
elementPosition = win * percent
|
||||||
|
}
|
||||||
|
|
||||||
|
return elementPosition
|
||||||
|
}
|
||||||
|
|
||||||
|
function draggify(elementId, margin) {
|
||||||
|
|
||||||
|
let element;
|
||||||
|
|
||||||
|
/* only compatible with elements having equal width and height */
|
||||||
|
let elementLength;
|
||||||
|
|
||||||
|
// Cords of inputItem.
|
||||||
|
const elementX = Vue.ref();
|
||||||
|
const elementY = Vue.ref();
|
||||||
|
|
||||||
|
// Position of inputItem on terms of percentage of window.
|
||||||
|
let percentX;
|
||||||
|
let percentY;
|
||||||
|
|
||||||
|
// How close inputItem is to closest X or Y side.
|
||||||
|
let xShort;
|
||||||
|
let yShort;
|
||||||
|
|
||||||
|
// Keep track of window size;
|
||||||
|
let winW = window.innerWidth;
|
||||||
|
let winH = window.innerHeight;
|
||||||
|
|
||||||
|
//---Reposition Anime-----------------------------------------------
|
||||||
|
|
||||||
|
// Move element to target smoothly.
|
||||||
|
const repositionAnime = (xChange, yChange) => {
|
||||||
|
const xStep = xChange / 6000;
|
||||||
|
const yStep = yChange / 6000;
|
||||||
|
|
||||||
|
for (let i = 1; i <= 6000; i++) {
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
elementX.value += xStep;
|
||||||
|
elementY.value += yStep;
|
||||||
|
}, 16) // 60fps
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//---Event Handlers-----------------------------------------------
|
||||||
|
|
||||||
|
// Update the position of inputItem on mouse dragging.
|
||||||
|
const onMouseMove = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const inputItemRect = element.getBoundingClientRect();
|
||||||
|
elementX.value = inputItemRect.left + e.movementX;
|
||||||
|
elementY.value = inputItemRect.top + e.movementY;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add an event listener for dragging.
|
||||||
|
const onMouseDown = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
window.addEventListener('mousemove', onMouseMove);
|
||||||
|
window.addEventListener('mouseup', onMouseUp);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update position references when user is done moving targetEl.
|
||||||
|
const onMouseUp = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
console.log("onMouseUp");
|
||||||
|
window.removeEventListener('mousemove', onMouseMove);
|
||||||
|
window.removeEventListener('mouseup', onMouseUp);
|
||||||
|
|
||||||
|
// See if and calculate reposition.
|
||||||
|
let x = 0; // vector change
|
||||||
|
let y = 0;
|
||||||
|
|
||||||
|
if (elementX.value < 0) {
|
||||||
|
x = (elementX.value - margin)*-1
|
||||||
|
}
|
||||||
|
|
||||||
|
if (elementX.value > winW - elementLength) {
|
||||||
|
const b = winW - elementLength - margin;
|
||||||
|
x = (elementX.value - b)*-1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reposition y
|
||||||
|
if (elementY.value < 0) {
|
||||||
|
y = (elementY.value - margin)*-1
|
||||||
|
}
|
||||||
|
|
||||||
|
if (elementY.value > winH - elementLength) {
|
||||||
|
const d = winH - elementLength - margin;
|
||||||
|
y = (elementY.value - d)*-1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reposition if needed.
|
||||||
|
if (x !== 0 || y !== 0) repositionAnime(x, y);
|
||||||
|
|
||||||
|
xShort = calcSideProximity(elementX.value, elementLength, winW);
|
||||||
|
yShort = calcSideProximity(elementY.value, elementLength, winH);
|
||||||
|
|
||||||
|
// Update percentages.
|
||||||
|
percentX = elementX.value / winW;
|
||||||
|
percentY = elementY.value / winH;
|
||||||
|
|
||||||
|
// Save position.
|
||||||
|
savePosition();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update position of targetEl on windowResize.
|
||||||
|
const onWindowResize = (_e) => {
|
||||||
|
|
||||||
|
// Update window dimensions.
|
||||||
|
winW = window.innerWidth;
|
||||||
|
winH = window.innerHeight;
|
||||||
|
|
||||||
|
elementX.value = calcPosition(elementX.value, elementLength, percentX, xShort, winW);
|
||||||
|
elementY.value = calcPosition(elementY.value, elementLength, percentY, yShort, winH);
|
||||||
|
|
||||||
|
savePosition();
|
||||||
|
}
|
||||||
|
|
||||||
|
const savePosition = () => {
|
||||||
|
window.localStorage.setItem(saveLocation, JSON.stringify({
|
||||||
|
x: elementX.value,
|
||||||
|
y: elementY.value
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
//---------------------------------------------------------------
|
||||||
|
|
||||||
|
Vue.onMounted(() => {
|
||||||
|
|
||||||
|
element = document.getElementById(elementId);
|
||||||
|
|
||||||
|
elementLength = element.offsetWidth;
|
||||||
|
|
||||||
|
// Initialize the positional references.
|
||||||
|
percentX = elementX.value / window.innerWidth;
|
||||||
|
percentY = elementY.value / window.innerHeight;
|
||||||
|
|
||||||
|
xShort = calcSideProximity(elementX.value, elementLength, window.innerWidth);
|
||||||
|
yShort = calcSideProximity(elementX.value, elementLength, window.innerHeight);
|
||||||
|
|
||||||
|
// Then, we can listen for window resize (and mousedown).
|
||||||
|
element.addEventListener("mousedown", onMouseDown);
|
||||||
|
window.addEventListener('resize', onWindowResize)
|
||||||
|
});
|
||||||
|
|
||||||
|
// remove event listeners on component dismount.
|
||||||
|
Vue.onUnmounted(() => {
|
||||||
|
element.removeEventListener('mousedown', onMouseDown);
|
||||||
|
window.removeEventListener('resize', onWindowResize)
|
||||||
|
window.removeEventListener('mouseup', onMouseUp)
|
||||||
|
window.removeEventListener('mousemove', onMouseMove);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Try loading initPosition, otherwise set default values
|
||||||
|
let initPosition;
|
||||||
|
const rawData = window.localStorage.getItem("saveLocation")
|
||||||
|
rawData ? initPosition = JSON.parse(rawData) : initPosition = defaultPosition;
|
||||||
|
|
||||||
|
elementX.value = initPosition.x;
|
||||||
|
elementY.value = initPosition.y;
|
||||||
|
|
||||||
|
return { elementX, elementY };
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default draggify;
|
||||||
24
src/render/components/control/scroll.js
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
let el;
|
||||||
|
|
||||||
|
const initScroll = (elementId) => {
|
||||||
|
el = document.getElementById(elementId);
|
||||||
|
el.addEventListener('adjust-scroll', adjustScroll);
|
||||||
|
window.addEventListener('resize', adjustScroll);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isBottom = () => {
|
||||||
|
if (el) {
|
||||||
|
return el.scrollHeight - el.clientHeight <= el.scrollTop + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const adjustScroll = () => {
|
||||||
|
if (el) {
|
||||||
|
el.scrollTo({
|
||||||
|
top: el.scrollHeight - el.clientHeight,
|
||||||
|
behavior: 'smooth'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default initScroll;
|
||||||
73
src/render/components/control/text.js
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
const inputLength = 230;
|
||||||
|
|
||||||
|
const metaKeys = [
|
||||||
|
"Tab",
|
||||||
|
"CapsLock",
|
||||||
|
"Shift",
|
||||||
|
"Control",
|
||||||
|
"Alt",
|
||||||
|
"Meta",
|
||||||
|
" ",
|
||||||
|
"ArrowLeft",
|
||||||
|
"ArrowRight",
|
||||||
|
"ArrowUp",
|
||||||
|
"ArrowDown",
|
||||||
|
"Enter",
|
||||||
|
"Backspace",
|
||||||
|
"Escape"
|
||||||
|
];
|
||||||
|
|
||||||
|
function useText(elementId, left, typing)
|
||||||
|
{
|
||||||
|
let el;
|
||||||
|
|
||||||
|
const show = Vue.ref(false);
|
||||||
|
const leftSide = Vue.ref(false);
|
||||||
|
|
||||||
|
const calcSide = () => {
|
||||||
|
window.innerWidth - left.value < inputLength ? leftSide.value = true : leftSide.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* handle user typing */
|
||||||
|
Vue.onMounted(() => {
|
||||||
|
|
||||||
|
el = document.getElementById(elementId);
|
||||||
|
|
||||||
|
window.addEventListener("keydown", (e) => {
|
||||||
|
|
||||||
|
if (!show.value && !metaKeys.includes(e.key))
|
||||||
|
{
|
||||||
|
show.value = true;
|
||||||
|
}
|
||||||
|
else if (show.value && ((el.value.length === 1 && e.key === "Backspace") || e.key === "Escape"))
|
||||||
|
{
|
||||||
|
show.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.key === "Enter" && el.value)
|
||||||
|
{
|
||||||
|
window.mainApi.send("message", {
|
||||||
|
text: el.value, audio: false });
|
||||||
|
show.value = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
Vue.watch(left, calcSide);
|
||||||
|
|
||||||
|
Vue.watch(show, (c, p_) => {
|
||||||
|
if (c) {
|
||||||
|
el.focus();
|
||||||
|
} else {
|
||||||
|
el.value = "";
|
||||||
|
el.blur();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
calcSide();
|
||||||
|
|
||||||
|
return { leftSide, show };
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useText;
|
||||||
|
|
@ -1,107 +0,0 @@
|
||||||
import { ref, Ref } from "vue";
|
|
||||||
import anime from "animejs";
|
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Given index and length of content, return message child status.
|
|
||||||
export function calcChild (index: number, len: number)
|
|
||||||
{
|
|
||||||
if (len === 1)
|
|
||||||
{
|
|
||||||
return "none";
|
|
||||||
}
|
|
||||||
else if (index === 0)
|
|
||||||
{
|
|
||||||
return "first-child";
|
|
||||||
}
|
|
||||||
else if (index === len - 1)
|
|
||||||
{
|
|
||||||
return "last-child";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return "middle-child";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,135 +0,0 @@
|
||||||
import { Ref, ref, watch, onMounted, onUnmounted } from "vue";
|
|
||||||
import { postMessage } from "@/render/ipc";
|
|
||||||
import { 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: Raw = {
|
|
||||||
text: textInput.value,
|
|
||||||
audio: 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
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
23
src/render/components/header.js
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
const Header =
|
||||||
|
{
|
||||||
|
setup() {
|
||||||
|
const onNavBar = (command) => window.mainApi.send('nav-bar', command);
|
||||||
|
return { onNavBar };
|
||||||
|
},
|
||||||
|
|
||||||
|
template: `
|
||||||
|
<button id="header-titlebar" />
|
||||||
|
|
||||||
|
<span id="header-menu">
|
||||||
|
<button
|
||||||
|
class="header-menu-button header-exit-button button"
|
||||||
|
@click.prevent="onNavBar('close')"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
class="header-menu-button header-min-button button"
|
||||||
|
@click.prevent="onNavBar('min')"
|
||||||
|
/>
|
||||||
|
</span> `
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Header;
|
||||||
|
|
@ -1,90 +0,0 @@
|
||||||
<template>
|
|
||||||
<button id="titlebar" />
|
|
||||||
<span id="menu">
|
|
||||||
<button
|
|
||||||
class="menuButton exitButton"
|
|
||||||
@click.prevent="postNavBarExit"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
class="menuButton minimizeButton"
|
|
||||||
@click.prevent="postNavBarMin"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
|
|
||||||
// import { postNavBarExit, postNavBarMin } from "@/render/ipc";
|
|
||||||
import { defineComponent } from "vue";
|
|
||||||
import { postNavBar } from "@/render/ipc";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: "Header",
|
|
||||||
setup() {
|
|
||||||
const postNavBarExit = () => postNavBar('close');
|
|
||||||
const postNavBarMin = () => postNavBar('min');
|
|
||||||
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>
|
|
||||||
39
src/render/components/input.js
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
import useText from "./control/text.js";
|
||||||
|
import draggify from "./control/draggify.js";
|
||||||
|
|
||||||
|
const Input =
|
||||||
|
{
|
||||||
|
setup()
|
||||||
|
{
|
||||||
|
const recording = Vue.ref(false);
|
||||||
|
|
||||||
|
const { elementX, elementY } = draggify("input", 15);
|
||||||
|
|
||||||
|
const { leftSide, show } = useText("input-text", elementX);
|
||||||
|
|
||||||
|
window.mainApi.on("record", (status) => recording.value = status);
|
||||||
|
|
||||||
|
return { elementX, elementY, recording, leftSide, show };
|
||||||
|
},
|
||||||
|
|
||||||
|
template: `
|
||||||
|
<div id="input" :class="{ audio: recording }"
|
||||||
|
:style="{ top: elementY + 'px', left: elementX + 'px' }"
|
||||||
|
>
|
||||||
|
|
||||||
|
<div id="input-icon"></div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
id="input-text" type="text"
|
||||||
|
:class="[
|
||||||
|
{ leftSide: leftSide },
|
||||||
|
{ show: show },
|
||||||
|
{ recording: recording }
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</div> `
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Input;
|
||||||
|
|
||||||
|
|
@ -1,141 +0,0 @@
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
id="input-item"
|
|
||||||
:class="{ playing: recording }"
|
|
||||||
:style="{ top: `${elementY}px`, left: `${elementX}px` }"
|
|
||||||
>
|
|
||||||
|
|
||||||
<!-- Show the profile photo in the center of the input item -->
|
|
||||||
<!-- Background shadow animates on record -->
|
|
||||||
<div id="profile">{{ profile.photo }}</div>
|
|
||||||
|
|
||||||
<!-- Show text input on key-down -->
|
|
||||||
<input
|
|
||||||
id="textInput"
|
|
||||||
type="text"
|
|
||||||
/>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
|
|
||||||
import { defineComponent, watch } from "vue";
|
|
||||||
import draggify from "@/render/composables/useDraggify";
|
|
||||||
import { profile } from "@/render/shared/profile";
|
|
||||||
import { recording } from "@/render/shared/audio";
|
|
||||||
import { animateAudioInput } from "@/render/components/controllers/helpers";
|
|
||||||
|
|
||||||
import useTextInputController from "@/render/components/controllers/inputItemControl";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: "InputItem",
|
|
||||||
|
|
||||||
setup() {
|
|
||||||
|
|
||||||
// Default values for position.
|
|
||||||
const xStart = 15;
|
|
||||||
const yStart = window.innerHeight - 200;
|
|
||||||
|
|
||||||
// Calculate position of inputItem on drag.
|
|
||||||
const { elementX, elementY } = draggify("inputItem", xStart, yStart, 15);
|
|
||||||
|
|
||||||
// Controllers for text and audio.
|
|
||||||
const { typing } = useTextInputController(elementX)
|
|
||||||
|
|
||||||
// Control rec icon (red dot upper right of UI)
|
|
||||||
const { show, hide } = animateAudioInput();
|
|
||||||
watch(recording, (val, _oldval) => {
|
|
||||||
recording ? show() : hide();
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
profile,
|
|
||||||
elementX,
|
|
||||||
elementY,
|
|
||||||
recording
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
|
|
||||||
|
|
||||||
#input-item {
|
|
||||||
position: absolute;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
padding: 0;
|
|
||||||
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
|
|
||||||
z-index: 3;
|
|
||||||
|
|
||||||
// To prevent window drag when overlapping with titlebar.
|
|
||||||
-webkit-app-region: no-drag;
|
|
||||||
|
|
||||||
// border: 4px solid #C6C6C6;
|
|
||||||
border-radius: 50%;
|
|
||||||
|
|
||||||
// Set opacity here to not affect child.
|
|
||||||
background-color: rgba(235, 235, 235, 0.75);
|
|
||||||
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
box-shadow: 0px 0px 10px 0px rgba(0,0,0,0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
.rec-icon {
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background-color: red;
|
|
||||||
transform: translate(14px);
|
|
||||||
|
|
||||||
animation-name: rec;
|
|
||||||
animation-duration: 0.5s;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes rec {
|
|
||||||
from {
|
|
||||||
transform: translate(14px) scale(0.3);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: translate(14px) scale(1.0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#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>
|
|
||||||
58
src/render/components/login.js
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
const Login =
|
||||||
|
{
|
||||||
|
setup()
|
||||||
|
{
|
||||||
|
const usr = Vue.ref("");
|
||||||
|
const pwd = Vue.ref("");
|
||||||
|
|
||||||
|
const submitForm = async () => {
|
||||||
|
const success = await window.mainApi.invoke("login", usr.value, pwd.value);
|
||||||
|
if (!success) shake("login-form");
|
||||||
|
}
|
||||||
|
|
||||||
|
return { usr, pwd, submitForm };
|
||||||
|
},
|
||||||
|
|
||||||
|
template: `
|
||||||
|
<form id="login" @submit.prevent="submitForm">
|
||||||
|
|
||||||
|
<!-- username and password forms -->
|
||||||
|
<div id="login-form">
|
||||||
|
<!-- username -->
|
||||||
|
<div class="login-input-box">
|
||||||
|
<input class="login-input" type="text" v-model="usr" />
|
||||||
|
<div class="login-input-modifier">Email</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- password -->
|
||||||
|
<div class="login-input-box">
|
||||||
|
<input class="login-input" type="password" v-model="pwd" />
|
||||||
|
<div class="login-input-modifier">Password</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- submit button; position: fixed -->
|
||||||
|
<button
|
||||||
|
class="login-submit-button button"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Login
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</form> `
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Shake a div to indicate incorrect credentials */
|
||||||
|
const shake = (elementId) =>
|
||||||
|
{
|
||||||
|
const el = document.getElementById(elementId);
|
||||||
|
|
||||||
|
if (el)
|
||||||
|
el.classList.remove("shake");
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (el) el.classList.add("shake");
|
||||||
|
}, 250);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Login;
|
||||||
|
|
@ -1,173 +0,0 @@
|
||||||
<template>
|
|
||||||
<form id="login" @submit.prevent="submitForm">
|
|
||||||
|
|
||||||
<!-- username and password forms -->
|
|
||||||
<div id="form">
|
|
||||||
<!-- username -->
|
|
||||||
<div class="inputBox">
|
|
||||||
<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="pwd" />
|
|
||||||
<div class="inputModifier">Password</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- submit button; position: fixed -->
|
|
||||||
<button class="submit button" type="submit">Login</button>
|
|
||||||
|
|
||||||
</form>
|
|
||||||
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
|
|
||||||
import { defineComponent, ref } from "vue";
|
|
||||||
import { invokeLogin } from "@/render/ipc";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: "Login",
|
|
||||||
|
|
||||||
setup() {
|
|
||||||
|
|
||||||
const usr = ref("");
|
|
||||||
const pwd = ref("");
|
|
||||||
|
|
||||||
// Submit login credentials to the backend.
|
|
||||||
const submitForm = async () => {
|
|
||||||
|
|
||||||
try {
|
|
||||||
await invokeLogin({email: usr.value, password: pwd.value});
|
|
||||||
} catch(e) {
|
|
||||||
shake("form");
|
|
||||||
console.log(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
const shake = (elementId: string) => {
|
|
||||||
const el = document.getElementById(elementId);
|
|
||||||
|
|
||||||
if (el)
|
|
||||||
el.classList.remove("shake");
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
if (el) el.classList.add("shake");
|
|
||||||
}, 250);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
usr,
|
|
||||||
pwd,
|
|
||||||
submitForm
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
#login {
|
|
||||||
width: 100vw;
|
|
||||||
height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
#form {
|
|
||||||
font-family: "SF Compact Display";
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inputBox {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input {
|
|
||||||
background-color: #B9B9B9;
|
|
||||||
border: none;
|
|
||||||
outline: none;
|
|
||||||
padding: 16px;
|
|
||||||
text-decoration: none;
|
|
||||||
margin: 4px 2px;
|
|
||||||
cursor: pointer;
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inputModifier {
|
|
||||||
min-width: 181px;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: bold;
|
|
||||||
text-align: left;
|
|
||||||
margin-left: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.submit {
|
|
||||||
font-family: "SF Compact Display";
|
|
||||||
position: fixed;
|
|
||||||
margin-top: 260px;
|
|
||||||
background-color: #58C4FD;
|
|
||||||
color: white;
|
|
||||||
padding: 10px 30px;
|
|
||||||
border-radius: 20px;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.submit:active {
|
|
||||||
background-color: #4296C3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button {
|
|
||||||
border: none;
|
|
||||||
outline: none;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.shake {
|
|
||||||
animation: shake 0.82s cubic-bezier(.36,.07,.19,.97) both;
|
|
||||||
transform: translate3d(0, 0, 0);
|
|
||||||
backface-visibility: hidden;
|
|
||||||
perspective: 1000px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes shake {
|
|
||||||
10%, 90% {
|
|
||||||
transform: translate3d(-1px, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
20%, 80% {
|
|
||||||
transform: translate3d(2px, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
30%, 50%, 70% {
|
|
||||||
transform: translate3d(-4px, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
40%, 60% {
|
|
||||||
transform: translate3d(4px, 0, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
||||||
69
src/render/components/message.js
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
import Bubble from "./bubble.js";
|
||||||
|
import Context from "./context.js";
|
||||||
|
|
||||||
|
const Message =
|
||||||
|
{
|
||||||
|
components: {
|
||||||
|
Bubble,
|
||||||
|
Context
|
||||||
|
},
|
||||||
|
|
||||||
|
props: ["modifier", "content", "context", "avatar", "audio", "id"],
|
||||||
|
|
||||||
|
setup(props)
|
||||||
|
{
|
||||||
|
Vue.onMounted(() => {
|
||||||
|
|
||||||
|
const messengerEl = document.getElementById("messenger");
|
||||||
|
|
||||||
|
if (messengerEl) {
|
||||||
|
messengerEl.dispatchEvent(new CustomEvent('adjust-scroll'));
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
return { calcChild };
|
||||||
|
},
|
||||||
|
|
||||||
|
template: `
|
||||||
|
<div :id="id" :class="'message ' + modifier + '-message'">
|
||||||
|
|
||||||
|
<Bubble
|
||||||
|
v-for="(text, index) in content"
|
||||||
|
:modifier="modifier"
|
||||||
|
:text="text"
|
||||||
|
:child="calcChild(index, content.length)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Context
|
||||||
|
:modifier="modifier"
|
||||||
|
:context="context"
|
||||||
|
:avatar="avatar"
|
||||||
|
:audio="audio"
|
||||||
|
:id="id"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</div> `
|
||||||
|
}
|
||||||
|
|
||||||
|
function calcChild(index, len)
|
||||||
|
{
|
||||||
|
if (len === 1)
|
||||||
|
{
|
||||||
|
return "none-child";
|
||||||
|
}
|
||||||
|
else if (index === 0)
|
||||||
|
{
|
||||||
|
return "first-child";
|
||||||
|
}
|
||||||
|
else if (index === len - 1)
|
||||||
|
{
|
||||||
|
return "last-child";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return "middle-child";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Message;
|
||||||
|
|
@ -1,105 +0,0 @@
|
||||||
<template>
|
|
||||||
<div :id="uid" :class="`${modifier}-message`">
|
|
||||||
|
|
||||||
<Bubble
|
|
||||||
v-for="(val, index) in content"
|
|
||||||
:modifier="modifier"
|
|
||||||
:text="val.text"
|
|
||||||
:child="calcChild(index, content.length)"
|
|
||||||
:seen="seen"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Context
|
|
||||||
:modifier="modifier"
|
|
||||||
:context="context"
|
|
||||||
:uid="uid"
|
|
||||||
/>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang='ts'>
|
|
||||||
|
|
||||||
import { defineComponent, onMounted } from 'vue';
|
|
||||||
import Bubble from "@/render/components/bubble.vue";
|
|
||||||
import Context from "@/render/components/context.vue";
|
|
||||||
import { calcChild } from "@/render/components/controllers/helpers";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: "Message",
|
|
||||||
|
|
||||||
props: ["modifier", "content", "context", "seen", "uid", "playback"],
|
|
||||||
|
|
||||||
components: {
|
|
||||||
Bubble,
|
|
||||||
Context
|
|
||||||
},
|
|
||||||
|
|
||||||
setup(props) {
|
|
||||||
|
|
||||||
watch(props.playback, (val, _oldval) =>
|
|
||||||
{
|
|
||||||
boxShadow = val/200;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
|
|
||||||
const messengerEl = document.getElementById("messenger");
|
|
||||||
|
|
||||||
if (messengerEl)
|
|
||||||
messengerEl.dispatchEvent(new CustomEvent('adjust-scroll'))
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
calcChild
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
|
|
||||||
.message {
|
|
||||||
width: 100vw;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
padding-top: 9px;
|
|
||||||
padding-bottom: 9px;
|
|
||||||
animation-name: message-init-anim;
|
|
||||||
animation-duration: 0.25s;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes message-init-anim {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
} to {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.message:first-child {
|
|
||||||
margin-top: 55px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message:last-child {
|
|
||||||
margin-bottom: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.client-message {
|
|
||||||
@extend .message;
|
|
||||||
align-items: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ai-message {
|
|
||||||
@extend .message;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|
||||||
|
|
||||||
71
src/render/components/messenger.js
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
import initScroll from "./control/scroll.js";
|
||||||
|
|
||||||
|
import WS from "./ws.js";
|
||||||
|
import Input from "./input.js";
|
||||||
|
import Settings from "./settings.js";
|
||||||
|
import Message from "./message.js";
|
||||||
|
|
||||||
|
const Messenger = {
|
||||||
|
|
||||||
|
components: {
|
||||||
|
WS,
|
||||||
|
Settings,
|
||||||
|
Message,
|
||||||
|
Input
|
||||||
|
},
|
||||||
|
|
||||||
|
setup()
|
||||||
|
{
|
||||||
|
const messages = Vue.ref([]);
|
||||||
|
|
||||||
|
Vue.onMounted(() => {
|
||||||
|
|
||||||
|
window.mainApi.on("ui-init", (ui) => {
|
||||||
|
messages.value = ui;
|
||||||
|
});
|
||||||
|
|
||||||
|
window.mainApi.on("ui-update", (message) => {
|
||||||
|
console.log(message)
|
||||||
|
|
||||||
|
let index = messages.value.findIndex(m => m.id===message.id);
|
||||||
|
if (index >= 0)
|
||||||
|
{
|
||||||
|
messages.value[index] = message;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
messages.value.push(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
initScroll("messenger");
|
||||||
|
window.mainApi.send("messenger");
|
||||||
|
});
|
||||||
|
|
||||||
|
Vue.onUnmounted(() => {
|
||||||
|
messages.value = [];
|
||||||
|
});
|
||||||
|
|
||||||
|
return { messages };
|
||||||
|
},
|
||||||
|
|
||||||
|
template: `
|
||||||
|
<Settings />
|
||||||
|
<WS />
|
||||||
|
<Input/>
|
||||||
|
|
||||||
|
<div id="messenger">
|
||||||
|
<Message
|
||||||
|
v-for="message in messages"
|
||||||
|
:modifier="message.modifier"
|
||||||
|
:content="message.content"
|
||||||
|
:context="message.context"
|
||||||
|
:avatar="message.avatar"
|
||||||
|
:audio="message.audio"
|
||||||
|
:id="message.id"
|
||||||
|
/>
|
||||||
|
</div> `
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Messenger;
|
||||||
|
|
@ -1,126 +0,0 @@
|
||||||
<template>
|
|
||||||
|
|
||||||
<!-- Position fixed items -->
|
|
||||||
<Settings />
|
|
||||||
<ConnectionStatus />
|
|
||||||
<div id="recIcon" />
|
|
||||||
<InputItem />
|
|
||||||
|
|
||||||
<!-- List of message bubbles. -->
|
|
||||||
<div id="messenger">
|
|
||||||
<Message
|
|
||||||
v-for="message in messages"
|
|
||||||
:modifier="message.modifier"
|
|
||||||
:content="message.content"
|
|
||||||
:context="message.context"
|
|
||||||
:seen="message.seen"
|
|
||||||
:uid="message.uid"
|
|
||||||
/>
|
|
||||||
</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 Message from "@/render/components/message.vue";
|
|
||||||
import ConnectionStatus from "./connectionStatus.vue";
|
|
||||||
|
|
||||||
import { profile } from "@/render/shared/profile";
|
|
||||||
import { messages } from "@/render/shared/messages";
|
|
||||||
import useScroll from "@/render/composables/useScroll";
|
|
||||||
import { postMessage, postWindowFocus } from "@/render/ipc";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: "Messenger",
|
|
||||||
|
|
||||||
components: {
|
|
||||||
ConnectionStatus,
|
|
||||||
InputItem,
|
|
||||||
Settings,
|
|
||||||
Message,
|
|
||||||
},
|
|
||||||
|
|
||||||
setup() {
|
|
||||||
|
|
||||||
const onFocus = () => {
|
|
||||||
postWindowFocus(true);
|
|
||||||
postMessage({
|
|
||||||
intent: "focus",
|
|
||||||
params: { "focus": true },
|
|
||||||
epic: false,
|
|
||||||
confidence: 1.0
|
|
||||||
} as PlatformRequest);
|
|
||||||
}
|
|
||||||
|
|
||||||
const onBlur = () => {
|
|
||||||
postWindowFocus(false);
|
|
||||||
postMessage({
|
|
||||||
intent: "focus",
|
|
||||||
params: { "focus": false },
|
|
||||||
epic: false,
|
|
||||||
confidence: 1.0
|
|
||||||
} as PlatformRequest);
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
|
|
||||||
useScroll("messenger");
|
|
||||||
|
|
||||||
window.addEventListener('blur', onBlur);
|
|
||||||
window.addEventListener('focus', onFocus);
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
|
|
||||||
window.removeEventListener("blur", onBlur);
|
|
||||||
window.removeEventListener("focus", onFocus);
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
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>
|
|
||||||
40
src/render/components/settings.js
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
|
||||||
|
const Settings =
|
||||||
|
{
|
||||||
|
setup() {
|
||||||
|
const active = Vue.ref(false);
|
||||||
|
|
||||||
|
function hideSettings(e) {
|
||||||
|
if (e.key == "Escape") {
|
||||||
|
active.value = false;
|
||||||
|
window.removeEventListener("keydown", hideSettings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showSettings() {
|
||||||
|
active.value = true;
|
||||||
|
window.addEventListener("keydown", hideSettings);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function logout() {
|
||||||
|
await window.mainApi.send("logout");
|
||||||
|
}
|
||||||
|
|
||||||
|
return { showSettings, active, logout };
|
||||||
|
},
|
||||||
|
|
||||||
|
template: `
|
||||||
|
<div v-if="active" id="settings" >
|
||||||
|
|
||||||
|
<button class="settings-logout-button button" @click="logout">
|
||||||
|
Logout
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button v-else class="settings-icon" @click="showSettings">
|
||||||
|
<img src="./assets/settingsIcon.svg">
|
||||||
|
</button> `
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Settings;
|
||||||
|
|
@ -1,158 +0,0 @@
|
||||||
<template>
|
|
||||||
|
|
||||||
<!-- Settings content -->
|
|
||||||
<div v-if="toggleSettings" id="settings" >
|
|
||||||
|
|
||||||
<p class="account">{{ `Hi, ${account}` }}</p>
|
|
||||||
|
|
||||||
<button
|
|
||||||
class="logout"
|
|
||||||
@click="onLogout"
|
|
||||||
>
|
|
||||||
Logout
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<p class="version">{{ `Version ${version}` }}</p>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Icon -->
|
|
||||||
<button
|
|
||||||
v-else
|
|
||||||
class="icon"
|
|
||||||
@click="onActive"
|
|
||||||
>
|
|
||||||
<img src="@/render/assets/settingsIcon.svg">
|
|
||||||
</button>
|
|
||||||
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
|
|
||||||
import { defineComponent, ref } from "vue";
|
|
||||||
import { invokeLogout } from "@/render/ipc";
|
|
||||||
import { version } from "@/render/shared/version";
|
|
||||||
import { ready, account, setAccount, clearAccount } from "@/render/shared/account";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: "Settings",
|
|
||||||
|
|
||||||
setup() {
|
|
||||||
const toggleSettings = ref(false);
|
|
||||||
|
|
||||||
// Listen for escape key to close settings.
|
|
||||||
const onEscape = (e: any) => {
|
|
||||||
if(e.key === "Escape") {
|
|
||||||
toggleSettings.value = false;
|
|
||||||
}
|
|
||||||
window.removeEventListener("keydown", onEscape);
|
|
||||||
}
|
|
||||||
|
|
||||||
// When settings is showing.
|
|
||||||
const onActive = () => {
|
|
||||||
toggleSettings.value = true;
|
|
||||||
window.addEventListener("keydown", onEscape);
|
|
||||||
}
|
|
||||||
|
|
||||||
// We ask server to log us out.
|
|
||||||
const onLogout = async () => {
|
|
||||||
|
|
||||||
console.log("Submitting logout request.");
|
|
||||||
|
|
||||||
try {
|
|
||||||
await invokeLogout();
|
|
||||||
} catch(e) {
|
|
||||||
console.log('error')
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
account,
|
|
||||||
version,
|
|
||||||
onActive,
|
|
||||||
toggleSettings,
|
|
||||||
onLogout
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
|
|
||||||
#settings {
|
|
||||||
position: fixed;
|
|
||||||
width: 100vw;
|
|
||||||
height: 100vh;
|
|
||||||
background-color: rgba(235, 235, 235, 0.75);
|
|
||||||
z-index: 4;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
animation-name: appear;
|
|
||||||
animation-duration: 0.5s;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes appear {
|
|
||||||
from {
|
|
||||||
background-color: rgba(235, 235, 235, 0);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
background-color: rgba(235, 235, 235, 0.75);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
position: fixed;
|
|
||||||
right: 0;
|
|
||||||
border: none;
|
|
||||||
outline: none;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
padding: 5px;
|
|
||||||
margin-right: 20px;
|
|
||||||
margin-top: 19px;
|
|
||||||
z-index: 2;
|
|
||||||
background-color: Transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.account {
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: bold;
|
|
||||||
margin-bottom: 25px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logout {
|
|
||||||
font-family: "SF Compact Display";
|
|
||||||
background-color: #B7B7B7;
|
|
||||||
padding: 10px 30px;
|
|
||||||
border-radius: 20px;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: bold;
|
|
||||||
border: none;
|
|
||||||
outline: none;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logout:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.version {
|
|
||||||
position: absolute;
|
|
||||||
left: 50%;
|
|
||||||
top: 75%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #575757;
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|
||||||
50
src/render/components/splash.js
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
const Splash =
|
||||||
|
{
|
||||||
|
template: `
|
||||||
|
<div id="splash">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="57.046"
|
||||||
|
height="52.759"
|
||||||
|
viewBox="0 0 57.046 52.759"
|
||||||
|
>
|
||||||
|
<g transform="translate(-4127 -507)">
|
||||||
|
<g transform="translate(3949 332.206)">
|
||||||
|
<g transform="translate(178 174.794)">
|
||||||
|
<g transform="translate(0 27.405)">
|
||||||
|
<circle
|
||||||
|
cx="12.677"
|
||||||
|
cy="12.677"
|
||||||
|
r="12.677"
|
||||||
|
transform="translate(0 0)"
|
||||||
|
fill="#383838"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
cx="12.677"
|
||||||
|
cy="12.677"
|
||||||
|
r="12.677"
|
||||||
|
transform="translate(31.692 0)"
|
||||||
|
fill="#383838"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
<circle
|
||||||
|
cx="12.677"
|
||||||
|
cy="12.677"
|
||||||
|
r="12.677"
|
||||||
|
transform="translate(15.822 0)"
|
||||||
|
fill="#383838"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M36.27,83.67"
|
||||||
|
transform="translate(-23.569 -43.588)"
|
||||||
|
fill="#ff0"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Splash;
|
||||||
|
|
@ -1,69 +0,0 @@
|
||||||
<template>
|
|
||||||
<div id="splash">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="57.046"
|
|
||||||
height="52.759"
|
|
||||||
viewBox="0 0 57.046 52.759"
|
|
||||||
>
|
|
||||||
<g transform="translate(-4127 -507)">
|
|
||||||
<g transform="translate(3949 332.206)">
|
|
||||||
<g transform="translate(178 174.794)">
|
|
||||||
<g transform="translate(0 27.405)">
|
|
||||||
<circle
|
|
||||||
cx="12.677"
|
|
||||||
cy="12.677"
|
|
||||||
r="12.677"
|
|
||||||
transform="translate(0 0)"
|
|
||||||
fill="#383838"
|
|
||||||
/>
|
|
||||||
<circle
|
|
||||||
cx="12.677"
|
|
||||||
cy="12.677"
|
|
||||||
r="12.677"
|
|
||||||
transform="translate(31.692 0)"
|
|
||||||
fill="#383838"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
<circle
|
|
||||||
cx="12.677"
|
|
||||||
cy="12.677"
|
|
||||||
r="12.677"
|
|
||||||
transform="translate(15.822 0)"
|
|
||||||
fill="#383838"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M36.27,83.67"
|
|
||||||
transform="translate(-23.569 -43.588)"
|
|
||||||
fill="#ff0"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
|
|
||||||
import { defineComponent } from "vue";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: "Splash",
|
|
||||||
});
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
|
|
||||||
#splash {
|
|
||||||
width: 100vw;
|
|
||||||
height: 100vh;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
||||||
36
src/render/components/ws.js
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
|
||||||
|
const WS =
|
||||||
|
{
|
||||||
|
setup()
|
||||||
|
{
|
||||||
|
const status = Vue.ref();
|
||||||
|
|
||||||
|
Vue.onMounted(() => {
|
||||||
|
|
||||||
|
window.mainApi.on("ws-status", (val) => {
|
||||||
|
status.value = val;
|
||||||
|
console.log(status.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
return { status }
|
||||||
|
},
|
||||||
|
|
||||||
|
template: `
|
||||||
|
<div id="ws">
|
||||||
|
|
||||||
|
<img id="ws-logo" src="./assets/connectLogo.svg"
|
||||||
|
:class="{ move: status }"
|
||||||
|
>
|
||||||
|
|
||||||
|
<div id="ws-dots"
|
||||||
|
:class="{ move: status }"
|
||||||
|
>
|
||||||
|
<div class="ws-dot"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div> `
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WS;
|
||||||
|
|
@ -1,199 +0,0 @@
|
||||||
import { onMounted, ref, onUnmounted } from "vue";
|
|
||||||
|
|
||||||
//---Dragabble Helper Funcs--------------------------------------
|
|
||||||
|
|
||||||
// Calculate distance to nearest side.
|
|
||||||
function calcSideProximity (elementX: number, winW: number) {
|
|
||||||
|
|
||||||
// Calc right short.
|
|
||||||
let short = winW - elementX - 40;
|
|
||||||
|
|
||||||
// See if it's left short.
|
|
||||||
if (elementX + 20 < winW / 2) {
|
|
||||||
short = elementX
|
|
||||||
}
|
|
||||||
|
|
||||||
return short
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update elementX or elementY value on window resize
|
|
||||||
function calcPosition (elementPosition: number, percent: number,
|
|
||||||
short: number, win: number) {
|
|
||||||
|
|
||||||
// Is it close to the right/bottom side?
|
|
||||||
if (percent > 0.75) {
|
|
||||||
elementPosition = win - short - 40;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Is it not close to a side?
|
|
||||||
if (percent < 0.75 && percent > 0.25) {
|
|
||||||
elementPosition = win * percent
|
|
||||||
}
|
|
||||||
|
|
||||||
return elementPosition
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function draggify(elementId: string, xStart: number,
|
|
||||||
yStart: number, margin: number) {
|
|
||||||
|
|
||||||
let element: HTMLElement | null;
|
|
||||||
|
|
||||||
// Cords of inputItem.
|
|
||||||
const elementX = ref(0);
|
|
||||||
const elementY = ref(0);
|
|
||||||
|
|
||||||
// Position of inputItem on terms of percentage of window.
|
|
||||||
let percentX: number;
|
|
||||||
let percentY: number;
|
|
||||||
|
|
||||||
// How close inputItem is to closest X or Y side.
|
|
||||||
let xShort: number;
|
|
||||||
let yShort: number;
|
|
||||||
|
|
||||||
// Keep track of window size;
|
|
||||||
let winW = window.innerWidth;
|
|
||||||
let winH = window.innerHeight;
|
|
||||||
|
|
||||||
//---Reposition Anime-----------------------------------------------
|
|
||||||
|
|
||||||
// Move element to target smoothly.
|
|
||||||
const repositionAnime = (xChange: number, yChange: number) => {
|
|
||||||
const xStep = xChange / 6000;
|
|
||||||
const yStep = yChange / 6000;
|
|
||||||
|
|
||||||
for (let i = 1; i <= 6000; i++) {
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
elementX.value += xStep;
|
|
||||||
elementY.value += yStep;
|
|
||||||
}, 16) // 60fps
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//---Event Handlers-----------------------------------------------
|
|
||||||
|
|
||||||
// Update the position of inputItem on mouse dragging.
|
|
||||||
const onMouseMove = (e: any) => {
|
|
||||||
e.preventDefault()
|
|
||||||
element = document.getElementById(elementId);
|
|
||||||
|
|
||||||
if (element) {
|
|
||||||
const inputItemRect = element.getBoundingClientRect();
|
|
||||||
elementX.value = inputItemRect.left + e.movementX;
|
|
||||||
elementY.value = inputItemRect.top + e.movementY;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add an event listener for dragging.
|
|
||||||
const onMouseDown = (_e: any) => {
|
|
||||||
_e.preventDefault()
|
|
||||||
window.addEventListener('mousemove', onMouseMove, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update position references when user is done moving targetEl.
|
|
||||||
const onMouseUp = (_e: any) => {
|
|
||||||
window.removeEventListener('mousemove', onMouseMove, true);
|
|
||||||
|
|
||||||
// See if and calculate reposition.
|
|
||||||
let x = 0; // vector change
|
|
||||||
let y = 0;
|
|
||||||
|
|
||||||
if (elementX.value < 0) {
|
|
||||||
x = (elementX.value - margin)*-1
|
|
||||||
}
|
|
||||||
|
|
||||||
if (elementX.value > winW - 40) {
|
|
||||||
const b = winW - 40 - margin;
|
|
||||||
x = (elementX.value - b)*-1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reposition y
|
|
||||||
if (elementY.value < 0) {
|
|
||||||
y = (elementY.value - margin)*-1
|
|
||||||
}
|
|
||||||
|
|
||||||
if (elementY.value > winH - 40) {
|
|
||||||
const d = winH - 40 - margin;
|
|
||||||
y = (elementY.value - d)*-1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reposition if needed.
|
|
||||||
if (x !== 0 || y !== 0) repositionAnime(x, y);
|
|
||||||
|
|
||||||
xShort = calcSideProximity(elementX.value, winW);
|
|
||||||
yShort = calcSideProximity(elementY.value, winH);
|
|
||||||
|
|
||||||
// Update percentages.
|
|
||||||
percentX = elementX.value / winW;
|
|
||||||
percentY = elementY.value / winH;
|
|
||||||
|
|
||||||
// Save position.
|
|
||||||
const position = {
|
|
||||||
x: elementX.value,
|
|
||||||
y: elementY.value
|
|
||||||
}
|
|
||||||
window.localStorage.setItem("inputItem_position", JSON.stringify(position));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update position of targetEl on windowResize.
|
|
||||||
const onWindowResize = (_e: any) => {
|
|
||||||
|
|
||||||
// Update window dimensions.
|
|
||||||
winW = window.innerWidth;
|
|
||||||
winH = window.innerHeight;
|
|
||||||
|
|
||||||
elementX.value = calcPosition(elementX.value, percentX, xShort, winW);
|
|
||||||
elementY.value = calcPosition(elementY.value, percentY, yShort, winH);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
//---------------------------------------------------------------
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
element = document.getElementById(elementId);
|
|
||||||
|
|
||||||
if (element) {
|
|
||||||
element.addEventListener('mousedown', onMouseDown, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener("mouseup", onMouseUp, false);
|
|
||||||
|
|
||||||
// Initialize the positional references.
|
|
||||||
percentX = elementX.value / window.innerWidth;
|
|
||||||
percentY = elementY.value / window.innerHeight;
|
|
||||||
|
|
||||||
xShort = calcSideProximity(elementX.value, window.innerWidth);
|
|
||||||
yShort = calcSideProximity(elementX.value, window.innerHeight);
|
|
||||||
|
|
||||||
// Then, we can listen for window resize.
|
|
||||||
window.addEventListener('resize', onWindowResize, false);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Try loading initPosition, otherwise set default values
|
|
||||||
let initPosition: any;
|
|
||||||
const rawData = window.localStorage.getItem("inputItem_position")
|
|
||||||
|
|
||||||
if (rawData) {
|
|
||||||
initPosition = JSON.parse(rawData)
|
|
||||||
} else {
|
|
||||||
initPosition = {x: xStart, y: yStart}
|
|
||||||
}
|
|
||||||
|
|
||||||
elementX.value = initPosition.x;
|
|
||||||
elementY.value = initPosition.y;
|
|
||||||
|
|
||||||
// remove event listeners on component dismount.
|
|
||||||
onUnmounted(() => {
|
|
||||||
window.removeEventListener('resize', onWindowResize);
|
|
||||||
window.removeEventListener('mouseup', onMouseUp);
|
|
||||||
if (element) element.removeEventListener('mousedown', onMouseDown);
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
elementX,
|
|
||||||
elementY
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,54 +0,0 @@
|
||||||
|
|
||||||
import { IpcRendererEvent } from "electron";
|
|
||||||
|
|
||||||
export class IpcRendererListener<InputParam> implements IIpcListener<InputParam> {
|
|
||||||
|
|
||||||
readonly channel: string;
|
|
||||||
|
|
||||||
readonly _listenerCallback: IpcListenerCallback<InputParam>;
|
|
||||||
|
|
||||||
constructor(options: {
|
|
||||||
channel: string;
|
|
||||||
listenerCallback: IpcListenerCallback<InputParam>;
|
|
||||||
}) {
|
|
||||||
this.channel = options.channel;
|
|
||||||
this._listenerCallback = options.listenerCallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
listen() {
|
|
||||||
this.remove();
|
|
||||||
window.ipcRenderer.on(this.channel, this._onPost);
|
|
||||||
}
|
|
||||||
|
|
||||||
remove() {
|
|
||||||
window.ipcRenderer.removeAllListeners(this.channel);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _onPost = (_e: IpcRendererEvent, payload: InputParam): void => {
|
|
||||||
console.log(`[IPC] Post: ${this.channel}`, payload);
|
|
||||||
this._listenerCallback(payload);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export default function useIpcRenderer () {
|
|
||||||
|
|
||||||
const invoke = async <T>(endpoint: string, payload: T) => {
|
|
||||||
try {
|
|
||||||
const res = await window.ipcRenderer.invoke(endpoint, payload? JSON.stringify(payload) : null);
|
|
||||||
return res;
|
|
||||||
} catch (e) {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const post = <T>(endpoint: string, payload: T) => {
|
|
||||||
window.ipcRenderer.send(endpoint, payload);
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
invoke,
|
|
||||||
post,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
|
|
||||||
export default function useScroll(elementId: string) {
|
|
||||||
|
|
||||||
const el = document.getElementById(elementId);
|
|
||||||
|
|
||||||
const isBottom = () => {
|
|
||||||
if (el) {
|
|
||||||
return el.scrollHeight - el.clientHeight <= el.scrollTop + 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const adjustScroll = () => {
|
|
||||||
if (el) {
|
|
||||||
el.scrollTo({
|
|
||||||
top: el.scrollHeight - el.clientHeight,
|
|
||||||
behavior: 'smooth'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (el)
|
|
||||||
el.addEventListener('adjust-scroll', adjustScroll);
|
|
||||||
window.addEventListener('resize', adjustScroll);
|
|
||||||
|
|
||||||
}
|
|
||||||
17
src/render/index.html
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<link rel="stylesheet" href="./main.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div id="app"></div>
|
||||||
|
|
||||||
|
<!-- vue@3.2.11 -->
|
||||||
|
<script src="./vendor/vue.js"></script>
|
||||||
|
|
||||||
|
<script type="module" src="./index.js"></script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
2
src/render/index.js
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
import App from "./components/app.js";
|
||||||
|
Vue.createApp(App).mount("#app");
|
||||||
|
|
@ -1,90 +0,0 @@
|
||||||
|
|
||||||
import useIpc from "@/render/composables/useIpcRend";
|
|
||||||
import * as rendererListeners from "./listeners";
|
|
||||||
|
|
||||||
const { post, invoke } = useIpc();
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* Account and auth related endpoints
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const invokeLogin = async (
|
|
||||||
payload: AccountCredentials
|
|
||||||
): Promise<Profile | Error> => (
|
|
||||||
await invoke('invoke-account-login', payload)
|
|
||||||
);
|
|
||||||
|
|
||||||
export const invokeLogout = async (): Promise<void> => (
|
|
||||||
await invoke("invoke-account-logout", null)
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* Audio endpoints
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
// export const postPlayback = (audio: Int16Array, uid: string): void => (
|
|
||||||
// post('post-playback', payload)
|
|
||||||
// );
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* Crimata Platform (session) endpoints
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const postMessage = (payload: Raw | PlatformRequest): void => (
|
|
||||||
post('post-session-send', payload)
|
|
||||||
);
|
|
||||||
|
|
||||||
export const postAppMount = (): void => (
|
|
||||||
post('post-app-mount', null)
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* Nav bar endpoints
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const postNavBar = (payload: string): void => (
|
|
||||||
post('post-nav-bar', payload)
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* Window focus endpoint
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const postWindowFocus = (payload: boolean): void => (
|
|
||||||
post('post-window-focus', payload)
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* Ipc Renderer Listeners
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
let ipcListeners: IPCListeners = {};
|
|
||||||
|
|
||||||
export const initIpcRendererListeners = () => {
|
|
||||||
for (const [key, listener] of Object.entries(rendererListeners)) {
|
|
||||||
if (!(key in ipcListeners)) {
|
|
||||||
ipcListeners[key] = listener;
|
|
||||||
listener.listen();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const removeListeners = () => {
|
|
||||||
for (const [key, listener] of Object.entries(rendererListeners)) {
|
|
||||||
listener.remove();
|
|
||||||
}
|
|
||||||
ipcListeners = {};
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
@ -1,81 +0,0 @@
|
||||||
|
|
||||||
import { IpcRendererListener } from "./composables/useIpcRend"
|
|
||||||
/*
|
|
||||||
* Shared state imports
|
|
||||||
* */
|
|
||||||
import {
|
|
||||||
setMessages,
|
|
||||||
addMessage,
|
|
||||||
updateMessage,
|
|
||||||
deleteMessage,
|
|
||||||
animatePlayback
|
|
||||||
} from "./shared/messages";
|
|
||||||
import { setAccount } from "./shared/account";
|
|
||||||
import { setProfile } from "./shared/profile";
|
|
||||||
import { setVersion } from "./shared/version";
|
|
||||||
import { setConnectionStatus } from "./shared/connectionStatus";
|
|
||||||
import { setRecording } from "./shared/audio";
|
|
||||||
|
|
||||||
const SET_ACCOUNT_CHANNEL = "set-account";
|
|
||||||
const SET_PROFILE_CHANNEL = "set-profile";
|
|
||||||
const SET_VERSION_CHANNEL = "set-version";
|
|
||||||
|
|
||||||
const INIT_MESSAGES_CHANNEL = "init-messages";
|
|
||||||
const ADD_MESSAGE_CHANNEL = "add-message";
|
|
||||||
const UPDATE_MESSAGE_CHANNEL = "update-message";
|
|
||||||
const DELETE_MESSAGE_CHANNEL = "delete-message";
|
|
||||||
|
|
||||||
const CONNECTION_STATUS_CHANNEL = "set-connection-status";
|
|
||||||
|
|
||||||
const RECORDING_CHANNEL = "recording";
|
|
||||||
const PLAYBACK_CHANNEL = "playback";
|
|
||||||
|
|
||||||
export const setAccountListener = new IpcRendererListener({
|
|
||||||
channel: SET_ACCOUNT_CHANNEL,
|
|
||||||
listenerCallback: setAccount
|
|
||||||
});
|
|
||||||
|
|
||||||
export const setVersionListener = new IpcRendererListener({
|
|
||||||
channel: SET_VERSION_CHANNEL,
|
|
||||||
listenerCallback: setVersion
|
|
||||||
});
|
|
||||||
|
|
||||||
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
|
|
||||||
});
|
|
||||||
|
|
||||||
export const deleteMessagesListener = new IpcRendererListener({
|
|
||||||
channel: DELETE_MESSAGE_CHANNEL,
|
|
||||||
listenerCallback: deleteMessage
|
|
||||||
});
|
|
||||||
|
|
||||||
export const connectionStatusListener = new IpcRendererListener({
|
|
||||||
channel: CONNECTION_STATUS_CHANNEL,
|
|
||||||
listenerCallback: setConnectionStatus
|
|
||||||
});
|
|
||||||
|
|
||||||
export const recordingListener = new IpcRendererListener({
|
|
||||||
channel: RECORDING_CHANNEL,
|
|
||||||
listenerCallback: setRecording
|
|
||||||
});
|
|
||||||
|
|
||||||
export const playbackListener = new IpcRendererListener({
|
|
||||||
channel: PLAYBACK_CHANNEL,
|
|
||||||
listenerCallback: animatePlayback
|
|
||||||
});
|
|
||||||
507
src/render/main.css
Normal file
|
|
@ -0,0 +1,507 @@
|
||||||
|
html, body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The first word of the class is the component that it modifys. */
|
||||||
|
|
||||||
|
#splash {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
font-family: "SF Pro Text";
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
border-radius: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#header-titlebar {
|
||||||
|
position: fixed;
|
||||||
|
width: 100vw;
|
||||||
|
height: 54px;
|
||||||
|
opacity: 0.75;
|
||||||
|
background-color: #EBEBEB;
|
||||||
|
-webkit-app-region: drag;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#header-menu {
|
||||||
|
position: fixed;
|
||||||
|
margin-left: 20px;
|
||||||
|
margin-top: 20px;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-menu-button {
|
||||||
|
min-width: 14px;
|
||||||
|
min-height: 14px;
|
||||||
|
border-radius: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-exit-button {
|
||||||
|
background-color: #FF6157;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-exit-button:active {
|
||||||
|
background: #c14645;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-min-button {
|
||||||
|
background-color: #FFC12F;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-min-button:active {
|
||||||
|
background-color: #c08e38;
|
||||||
|
}
|
||||||
|
|
||||||
|
#login {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#login-form {
|
||||||
|
font-family: "SF Compact Display";
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-input-box {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-input {
|
||||||
|
background-color: #B9B9B9;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
padding: 16px;
|
||||||
|
text-decoration: none;
|
||||||
|
margin: 4px 2px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-input-modifier {
|
||||||
|
min-width: 181px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: left;
|
||||||
|
margin-left: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-submit-button {
|
||||||
|
font-family: "SF Compact Display";
|
||||||
|
position: fixed;
|
||||||
|
margin-top: 260px;
|
||||||
|
background-color: #58C4FD;
|
||||||
|
color: white;
|
||||||
|
padding: 10px 30px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-submit-button:active {
|
||||||
|
background-color: #4296C3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* generic shaking functionality */
|
||||||
|
.shake {
|
||||||
|
animation: shake 0.82s cubic-bezier(.36,.07,.19,.97) both;
|
||||||
|
transform: translate3d(0, 0, 0);
|
||||||
|
backface-visibility: hidden;
|
||||||
|
perspective: 1000px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shake {
|
||||||
|
10%, 90% {
|
||||||
|
transform: translate3d(-1px, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
20%, 80% {
|
||||||
|
transform: translate3d(2px, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
30%, 50%, 70% {
|
||||||
|
transform: translate3d(-4px, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
40%, 60% {
|
||||||
|
transform: translate3d(4px, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#messenger {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* hide native scrollbar */
|
||||||
|
#messenger::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ws {
|
||||||
|
position: fixed;
|
||||||
|
width: 100vw;
|
||||||
|
height: 54px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ws-logo {
|
||||||
|
transition: all 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ws-logo.move {
|
||||||
|
transform: translateX(15px);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ws-dots {
|
||||||
|
opacity: 0;
|
||||||
|
transition: all 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ws-dots.move {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(-15px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-dot {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #48E065;
|
||||||
|
position: relative;
|
||||||
|
transform: translateX(-15px);
|
||||||
|
animation: ws-dot-flashing 1s infinite linear alternate;
|
||||||
|
animation-delay: .25s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-dot::before, .ws-dot::after {
|
||||||
|
content: '';
|
||||||
|
display: inline-block;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-dot::before {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #48E065;
|
||||||
|
left: -9px;
|
||||||
|
animation: ws-dot-flashing 1s infinite alternate;
|
||||||
|
animation-delay: 0s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-dot::after {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #48E065;
|
||||||
|
left: 9px;
|
||||||
|
animation: ws-dot-flashing 1s infinite alternate;
|
||||||
|
animation-delay: 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes ws-dot-flashing {
|
||||||
|
0% {
|
||||||
|
background-color: #48E065;
|
||||||
|
}
|
||||||
|
50%,
|
||||||
|
100% {
|
||||||
|
background-color: #9B9B9B;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#input {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
z-index: 3;
|
||||||
|
/* To prevent window drag when overlapping with titlebar. */
|
||||||
|
-webkit-app-region: no-drag;
|
||||||
|
border-radius: 50%;
|
||||||
|
/* Set opacity here to not affect child. */
|
||||||
|
background-color: rgba(235, 235, 235, 0.75);
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0px 0px 10px 0px rgba(0,0,0,0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
#input-text {
|
||||||
|
position: absolute;
|
||||||
|
min-width: 150px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 18px;
|
||||||
|
padding: 10px;
|
||||||
|
outline: none;
|
||||||
|
border: none;
|
||||||
|
pointer-events: none;
|
||||||
|
background-color: white;
|
||||||
|
z-index: -1;
|
||||||
|
box-shadow: 0px 0px 10px 0px rgba(0,0,0,0.15);
|
||||||
|
transform-origin: center;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(70%);
|
||||||
|
transition: all 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
#input-text.show {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#input-text.leftSide {
|
||||||
|
transform: translateX(-70%);
|
||||||
|
}
|
||||||
|
|
||||||
|
#settings {
|
||||||
|
position: fixed;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
background-color: rgba(235, 235, 235, 0.75);
|
||||||
|
z-index: 4;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
animation-name: settings-appear;
|
||||||
|
animation-duration: 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes settings-appear {
|
||||||
|
from {
|
||||||
|
background-color: rgba(235, 235, 235, 0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
background-color: rgba(235, 235, 235, 0.75);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-icon {
|
||||||
|
position: fixed;
|
||||||
|
right: 0;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
padding: 5px;
|
||||||
|
margin-right: 20px;
|
||||||
|
margin-top: 19px;
|
||||||
|
z-index: 2;
|
||||||
|
background-color: Transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-icon:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-account {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-logout-button {
|
||||||
|
font-family: "SF Compact Display";
|
||||||
|
background-color: #B7B7B7;
|
||||||
|
padding: 10px 30px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-logout-button:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-version {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 75%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #575757;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
width: 100vw;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding-top: 9px;
|
||||||
|
padding-bottom: 9px;
|
||||||
|
animation-name: message-init-anim;
|
||||||
|
animation-duration: 0.25s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes message-init-anim {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
} to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.message:first-child {
|
||||||
|
margin-top: 55px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message:last-child {
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-message {
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-message {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bubble {
|
||||||
|
position: relative;
|
||||||
|
max-width: 66vw;
|
||||||
|
font-family: "SF Pro Text";
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 18px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-bubble {
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
margin-left: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-bubble {
|
||||||
|
color: white;
|
||||||
|
background-color: #58C4FD;
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-first-child {
|
||||||
|
border-bottom-left-radius: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-middle-child {
|
||||||
|
border-top-left-radius: 9px;
|
||||||
|
border-bottom-left-radius: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-last-child {
|
||||||
|
border-top-left-radius: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-first-child {
|
||||||
|
border-bottom-right-radius: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-middle-child {
|
||||||
|
border-top-right-radius: 9px;
|
||||||
|
border-bottom-right-radius: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-last-child {
|
||||||
|
border-top-right-radius: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bubble-notify {
|
||||||
|
position: absolute;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #58D9FF;
|
||||||
|
top: -5px;
|
||||||
|
left: -5px;
|
||||||
|
border: 2px solid #EBEBEB;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context {
|
||||||
|
position: relative;
|
||||||
|
min-height: 14px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-family: "SF Compact Display";
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-context {
|
||||||
|
margin-left: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-context {
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-avatar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 15px;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- shared between components ---------- */
|
||||||
|
|
||||||
|
.button {
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*.button:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}*/
|
||||||
|
|
||||||
|
.audio {
|
||||||
|
animation-name: audio-anim;
|
||||||
|
animation-duration: 2s;
|
||||||
|
animation-iteration-count: infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes audio-anim {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
box-shadow: 0 0 0 0.4em rgba(195, 195, 195, 0.4), 0 0 0 0.25em rgba(195, 195, 195, 0.15);
|
||||||
|
}
|
||||||
|
25% {
|
||||||
|
box-shadow: 0 0 0 0.15em rgba(195, 195, 195, 0.15), 0 0 0 0.4em rgba(195, 195, 195, 0.3);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
box-shadow: 0 0 0 0.55em rgba(195, 195, 195, 0.55), 0 0 0 0.15em rgba(195, 195, 195, 0.05);
|
||||||
|
}
|
||||||
|
75% {
|
||||||
|
box-shadow: 0 0 0 0.25em rgba(195, 195, 195, 0.25), 0 0 0 0.55em rgba(195, 195, 195, 0.45);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
|
|
||||||
// src/main.ts
|
|
||||||
|
|
||||||
import App from "./App.vue";
|
|
||||||
|
|
||||||
import mitt from "mitt";
|
|
||||||
import { createApp } from "vue";
|
|
||||||
|
|
||||||
import { initIpcRendererListeners } from "./ipc"
|
|
||||||
|
|
||||||
|
|
||||||
// Handle ipcMain events.
|
|
||||||
initIpcRendererListeners();
|
|
||||||
|
|
||||||
// Handle events.
|
|
||||||
const emitter = mitt();
|
|
||||||
|
|
||||||
const app = createApp(App);
|
|
||||||
|
|
||||||
app.provide("mitt", emitter);
|
|
||||||
app.mount("#app");
|
|
||||||
53
src/render/preload.js
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
const { contextBridge, ipcRenderer } = require("electron");
|
||||||
|
|
||||||
|
/** The main <-> render interface */
|
||||||
|
|
||||||
|
const validFromMainChannels = [
|
||||||
|
"set-account",
|
||||||
|
"ui-init",
|
||||||
|
"ui-update",
|
||||||
|
"ws-status",
|
||||||
|
"record",
|
||||||
|
"playback"
|
||||||
|
];
|
||||||
|
|
||||||
|
const validToMain = [
|
||||||
|
"messenger",
|
||||||
|
"message",
|
||||||
|
"nav-bar",
|
||||||
|
"login",
|
||||||
|
"logout"
|
||||||
|
]
|
||||||
|
|
||||||
|
ipcRenderer.setMaxListeners(250);
|
||||||
|
|
||||||
|
// Expose protected methods that allow the renderer process to use
|
||||||
|
// the ipcRenderer without exposing the entire object
|
||||||
|
// TODO: Implement argument filtering for added security
|
||||||
|
contextBridge.exposeInMainWorld(
|
||||||
|
"mainApi", {
|
||||||
|
invoke: async (channel, ...args) => {
|
||||||
|
if (validToMain.includes(channel)) {
|
||||||
|
try {
|
||||||
|
const res = await ipcRenderer.invoke(channel, ...args);
|
||||||
|
return res;
|
||||||
|
} catch (e) {
|
||||||
|
console.log("IPCRenderer.invoke error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
send: (channel, ...args) => {
|
||||||
|
if (validToMain.includes(channel)) {
|
||||||
|
ipcRenderer.send(channel, ...args);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
on: (channel, func) => {
|
||||||
|
if (validFromMainChannels.includes(channel)) {
|
||||||
|
ipcRenderer.on(channel, (event, ...args) => func(...args));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
removeAllListeners: (channel) => {
|
||||||
|
ipcRenderer.removeAllListeners(channel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
// All of the Node.js APIs are available in the preload process.
|
|
||||||
// It has the same sandbox as a Chrome extension.
|
|
||||||
import { ipcRenderer } from "electron";
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
ipcRenderer: typeof ipcRenderer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.ipcRenderer = ipcRenderer;
|
|
||||||
|
|
||||||
process.once("loaded", () => {
|
|
||||||
|
|
||||||
window.addEventListener("message", event => {
|
|
||||||
|
|
||||||
// do something with custom event
|
|
||||||
const message = event.data;
|
|
||||||
|
|
||||||
if (message.endpoint === "update-menu-bar") {
|
|
||||||
ipcRenderer.send("update-menu-bar", message.content);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.endpoint === "update-recorder") {
|
|
||||||
ipcRenderer.send("update-recorder", message.content);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.endpoint === "client-message") {
|
|
||||||
ipcRenderer.send("client-message", message.content);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.endpoint === "logout") {
|
|
||||||
ipcRenderer.send("logout", message.content);
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
// shared
|
|
||||||
import { ref } from "vue";
|
|
||||||
|
|
||||||
export const ready = ref(false);
|
|
||||||
export const account = ref();
|
|
||||||
|
|
||||||
export const setAccount: IpcListenerCallback<string | null> = (payload) => {
|
|
||||||
payload ? account.value = payload : clearAccount();
|
|
||||||
showRender();
|
|
||||||
};
|
|
||||||
|
|
||||||
export const clearAccount = () => {
|
|
||||||
console.log("Clearing account");
|
|
||||||
account.value = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const showRender = () => {
|
|
||||||
ready.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
ready,
|
|
||||||
account,
|
|
||||||
setAccount,
|
|
||||||
clearAccount
|
|
||||||
};
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
// shared
|
|
||||||
import { ref } from "vue";
|
|
||||||
|
|
||||||
export const recording = ref();
|
|
||||||
|
|
||||||
export const setRecording: IpcListenerCallback<boolean> = (payload) => {
|
|
||||||
console.log("Recording");
|
|
||||||
// recording.value = payload;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
recording
|
|
||||||
}
|
|
||||||
|
|
@ -1,65 +0,0 @@
|
||||||
// shared
|
|
||||||
import { ref } from "vue";
|
|
||||||
import anime from "animejs";
|
|
||||||
|
|
||||||
export const status = ref("");
|
|
||||||
|
|
||||||
export let hiddenState = true;
|
|
||||||
|
|
||||||
let animateLogo: anime.AnimeInstance;
|
|
||||||
let animateConnect: anime.AnimeInstance;
|
|
||||||
|
|
||||||
export function useAnim() {
|
|
||||||
|
|
||||||
animateLogo = anime({
|
|
||||||
targets: '#logo',
|
|
||||||
translateX: [0, 15],
|
|
||||||
duration: 500,
|
|
||||||
autoplay: false,
|
|
||||||
easing: 'easeInOutBack'
|
|
||||||
});
|
|
||||||
|
|
||||||
animateConnect = anime({
|
|
||||||
targets: '#connect',
|
|
||||||
translateX: [0, -15],
|
|
||||||
opacity: [0, 1],
|
|
||||||
duration: 500,
|
|
||||||
autoplay: false,
|
|
||||||
easing: 'easeInOutBack'
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export function show() {
|
|
||||||
if (animateLogo && animateConnect) {
|
|
||||||
animateLogo.direction = "normal";
|
|
||||||
animateConnect.direction = "normal";
|
|
||||||
animateLogo.play();
|
|
||||||
animateConnect.play();
|
|
||||||
hiddenState = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function hide() {
|
|
||||||
if (animateLogo && animateConnect) {
|
|
||||||
animateLogo.direction = "reverse";
|
|
||||||
animateConnect.direction = "reverse";
|
|
||||||
animateLogo.play();
|
|
||||||
animateConnect.play();
|
|
||||||
hiddenState = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const setConnectionStatus: IpcListenerCallback<string> = (payload) => {
|
|
||||||
status.value = payload;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default {
|
|
||||||
hiddenState,
|
|
||||||
useAnim,
|
|
||||||
status,
|
|
||||||
show,
|
|
||||||
hide
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,79 +0,0 @@
|
||||||
// shared
|
|
||||||
import { ref, Ref } from "vue";
|
|
||||||
|
|
||||||
export const messages: Ref<Array<Message>> = ref([]);
|
|
||||||
|
|
||||||
export const setMessages: IpcListenerCallback<Array<Message>> = (payload) => {
|
|
||||||
messages.value = payload;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const addMessage: IpcListenerCallback<Message> = (payload) => {
|
|
||||||
messages.value.push(payload as Message);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const updateMessage: IpcListenerCallback<Annotation> = (payload) => {
|
|
||||||
const annotation = payload as Annotation;
|
|
||||||
|
|
||||||
for (const i in messages.value) {
|
|
||||||
|
|
||||||
if (messages.value[i].uid == annotation.uid) {
|
|
||||||
|
|
||||||
if (annotation.name == "content") {
|
|
||||||
messages.value[i].content = annotation.data as Content[];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (annotation.name == "context") {
|
|
||||||
messages.value[i].context = annotation.data as Context;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (annotation.name == "seen") {
|
|
||||||
messages.value[i].seen = annotation.data as boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
let playbackId: ReturnType<typeof setTimeout>;
|
|
||||||
|
|
||||||
export const animatePlayback: IpcListenerCallback<any> = (payload) => {
|
|
||||||
console.log("animatePlayback");
|
|
||||||
// clearTimeout(playbackId);
|
|
||||||
|
|
||||||
// const context = payload as AnimationContext;
|
|
||||||
|
|
||||||
// const el = document.getElementById(context.uid);
|
|
||||||
|
|
||||||
// el.style.boxShadow = `0px 0px 0px 20px rgba(0,0,0,${payload.fft})`;
|
|
||||||
|
|
||||||
// /* box shadow will return to 0 in 300ms if no more playback is done */
|
|
||||||
// playbackId = setTimeout(() => {
|
|
||||||
// el.style.boxShadow = `0px 0px 0px 20px rgba(0,0,0,0)`;
|
|
||||||
// }, 300);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const deleteMessage: IpcListenerCallback<string> = (payload) => {
|
|
||||||
const uid = payload as string;
|
|
||||||
|
|
||||||
for (let i = 0; i < messages.value.length; i++) {
|
|
||||||
if (messages.value[i].uid === uid) {
|
|
||||||
messages.value.splice(i, 1);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export const resetMessages = () => {
|
|
||||||
messages.value = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
messages,
|
|
||||||
setMessages,
|
|
||||||
addMessage,
|
|
||||||
updateMessage,
|
|
||||||
animatePlayback
|
|
||||||
};
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
// shared
|
|
||||||
import { ref } from "vue";
|
|
||||||
|
|
||||||
class Profile implements Profile{
|
|
||||||
crimata_id = "";
|
|
||||||
name = "";
|
|
||||||
photo = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
export const profile = ref(new Profile());
|
|
||||||
|
|
||||||
export const setProfile: IpcListenerCallback<Profile> = (payload) => {
|
|
||||||
profile.value = payload;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default {
|
|
||||||
profile,
|
|
||||||
setProfile
|
|
||||||
};
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
// shared
|
|
||||||
import { ref } from "vue";
|
|
||||||
|
|
||||||
export const version = ref();
|
|
||||||
|
|
||||||
export const setVersion: IpcListenerCallback<Profile | null> = (payload) => {
|
|
||||||
version.value = payload;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default {
|
|
||||||
setVersion
|
|
||||||
}
|
|
||||||
13
src/render/shims-vue.d.ts
vendored
|
|
@ -1,13 +0,0 @@
|
||||||
declare module "*.vue" {
|
|
||||||
import { defineComponent } from "vue";
|
|
||||||
const component: ReturnType<typeof defineComponent>;
|
|
||||||
export default component;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare module "anime-js" {
|
|
||||||
namespace anime {
|
|
||||||
import * as anime from "animejs/lib/anime.es.js";
|
|
||||||
function anime(): void;
|
|
||||||
export = anime;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
15872
src/render/vendor/vue.js
vendored
Normal file
81
src/session.js
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
const { Notification, ipcMain } = require("electron");
|
||||||
|
|
||||||
|
const store = require("./utils/store");
|
||||||
|
const { backgroundMitt, ipcEmit } = require("./utils/emitter");
|
||||||
|
const { connectToPlatform, disconnectFromPlatform } = require("./io");
|
||||||
|
const { initAudio, terminateAudio, playback, streamState } = require("./audio");
|
||||||
|
|
||||||
|
let ui; /* wait to receive ui from the platform */
|
||||||
|
|
||||||
|
const updateQueue = [];
|
||||||
|
|
||||||
|
let processContentId;
|
||||||
|
|
||||||
|
backgroundMitt.on("ws-message", (message) =>
|
||||||
|
{
|
||||||
|
console.log(message)
|
||||||
|
if (message.hasOwnProperty("ui"))
|
||||||
|
{
|
||||||
|
console.log("INITING UI")
|
||||||
|
ui = message.ui;
|
||||||
|
ipcEmit("ui-init", ui);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
updateQueue.push(message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleMessage()
|
||||||
|
{
|
||||||
|
if (!streamState.pb && !streamState.rec) /* if user is not "busy" */
|
||||||
|
{
|
||||||
|
const message = updateQueue.shift();
|
||||||
|
if (message)
|
||||||
|
{
|
||||||
|
let index = ui.findIndex(m => m.id === message.id);
|
||||||
|
|
||||||
|
if (index >= 0)
|
||||||
|
{
|
||||||
|
ui[index] = message;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ui.push(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.modifier === "ai" && !store.get("focus"))
|
||||||
|
{
|
||||||
|
new Notification({title: message.context, body: message.content}).show();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.audio) playback(message.audio, message.id);
|
||||||
|
|
||||||
|
ipcEmit("ui-update", message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function launchSession(account)
|
||||||
|
{
|
||||||
|
connectToPlatform(account);
|
||||||
|
initAudio();
|
||||||
|
processContentId = setInterval(handleMessage, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
function endSession()
|
||||||
|
{
|
||||||
|
clearInterval(processContentId);
|
||||||
|
terminateAudio();
|
||||||
|
disconnectFromPlatform();
|
||||||
|
ui = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
ipcMain.on("messenger", () => {
|
||||||
|
if (ui) {
|
||||||
|
ipcEmit("ui-init", ui);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
exports.launchSession = launchSession;
|
||||||
|
exports.endSession = endSession;
|
||||||
|
|
@ -1,97 +0,0 @@
|
||||||
import { Notification } from 'electron';
|
|
||||||
|
|
||||||
import UIState from "@/ui";
|
|
||||||
import { winState } from "@/window";
|
|
||||||
import { ConnectToPlatform, DisconnectFromPlatform } from "@/io";
|
|
||||||
import { backgroundMitt, ipcEmit } from "@/composables/useEmitter";
|
|
||||||
import { InitAudio, TerminateAudio, playback, streamState } from "@/audio";
|
|
||||||
|
|
||||||
let processContentId: ReturnType<typeof setInterval>;
|
|
||||||
|
|
||||||
const contentQueue: Update[] = [];
|
|
||||||
|
|
||||||
const uiState = new UIState();
|
|
||||||
|
|
||||||
backgroundMitt.on("ws-message", (payload: string) =>
|
|
||||||
{
|
|
||||||
const message = JSON.parse(payload) as ClientProtocol;
|
|
||||||
|
|
||||||
if (message.header == "init")
|
|
||||||
{
|
|
||||||
uiState.set(message.body as Init);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
const update = message.body as Update;
|
|
||||||
|
|
||||||
if (update.notify)
|
|
||||||
{
|
|
||||||
contentQueue.push(update);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
uiState.update(update);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
/* launch a new session (the main process for authenticated users) */
|
|
||||||
function LaunchSession(platformKey: string)
|
|
||||||
{
|
|
||||||
/* connect to the platform */
|
|
||||||
ConnectToPlatform(platformKey);
|
|
||||||
|
|
||||||
InitAudio();
|
|
||||||
|
|
||||||
/* Process new content every 100ms */
|
|
||||||
processContentId = setInterval(() =>
|
|
||||||
{
|
|
||||||
if (!streamState.rec || !streamState.pb)
|
|
||||||
{
|
|
||||||
if (contentQueue.length > 0)
|
|
||||||
{
|
|
||||||
const update = contentQueue.shift() as Update;
|
|
||||||
|
|
||||||
uiState.update(update);
|
|
||||||
|
|
||||||
if (!winState.focus)
|
|
||||||
{
|
|
||||||
new Notification(update.notify as NotifyOptions).show();
|
|
||||||
}
|
|
||||||
|
|
||||||
let uid: string;
|
|
||||||
let content: Content[];
|
|
||||||
|
|
||||||
if (update.name == "add")
|
|
||||||
{
|
|
||||||
const message = update.data as Message;
|
|
||||||
content = message.content;
|
|
||||||
uid = message.uid;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
const annotation = update.data as Annotation;
|
|
||||||
content = annotation.data as Content[];
|
|
||||||
uid = annotation.uid;
|
|
||||||
}
|
|
||||||
|
|
||||||
const audio = content[-1].audio;
|
|
||||||
if (audio) playback(audio, uid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
function EndSession()
|
|
||||||
{
|
|
||||||
TerminateAudio();
|
|
||||||
|
|
||||||
DisconnectFromPlatform(1000, 'session-logout');
|
|
||||||
|
|
||||||
clearInterval(processContentId)
|
|
||||||
|
|
||||||
uiState.reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
export { LaunchSession, EndSession };
|
|
||||||
44
src/tray.js
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const { Tray } = require("electron");
|
||||||
|
|
||||||
|
const { backgroundMitt } = require("./utils/emitter");
|
||||||
|
|
||||||
|
let tray;
|
||||||
|
const TRAY_PATH = "./src/assets/tray/";
|
||||||
|
|
||||||
|
/* Current state of recording, playback, and ws connection */
|
||||||
|
const state = {
|
||||||
|
recording: false,
|
||||||
|
playback: false,
|
||||||
|
disconnect: 0 // whether we're experiencing connection issues
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the tray icon given a new state of an attribute.
|
||||||
|
* @param {"recording" | "playback" | "disconnect"} attribute
|
||||||
|
* @param {boolean} newState
|
||||||
|
*/
|
||||||
|
function updateTray(attribute, newState) {
|
||||||
|
|
||||||
|
state[attribute] = newState;
|
||||||
|
|
||||||
|
// convert boolean to number
|
||||||
|
const bi = (b) => b ? 1 : 0;
|
||||||
|
|
||||||
|
// Make sure we have the icon we're looking for.
|
||||||
|
tray.setImage(
|
||||||
|
TRAY_PATH +
|
||||||
|
`${bi(state.recording)}${bi(state.playback)}${bi(state.disconnect)}` +
|
||||||
|
".png"
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function initTray()
|
||||||
|
{
|
||||||
|
tray = new Tray(TRAY_PATH + "000" + ".png");
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.initTray = initTray;
|
||||||
|
exports.updateTray = updateTray;
|
||||||