production refactor

This commit is contained in:
Andrew Gundersen 2021-10-16 09:39:10 -05:00
commit 41528e7e27
115 changed files with 20719 additions and 22107 deletions

View file

@ -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
View file

@ -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
View 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}

View file

@ -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"

View file

@ -1,5 +0,0 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}

View file

@ -1,6 +0,0 @@
module.exports = {
preset: '@vue/cli-plugin-unit-jest/presets/typescript-and-babel',
transform: {
'^.+\\.vue$': 'vue-jest'
}
}

View file

@ -1,6 +0,0 @@
{
"typeAcquisition": {
"enable": true
}
}

2213
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -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"
} }

View file

@ -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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4 KiB

41
src/account.js Normal file
View 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;

View file

@ -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
View 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;

View file

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

View file

Before

Width:  |  Height:  |  Size: 4 KiB

After

Width:  |  Height:  |  Size: 4 KiB

Before After
Before After

BIN
src/assets/tray/001.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

View file

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Before After
Before After

BIN
src/assets/tray/010.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View file

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Before After
Before After

BIN
src/assets/tray/011.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View file

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Before After
Before After

BIN
src/assets/tray/100.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

View file

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Before After
Before After

BIN
src/assets/tray/101.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

View file

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Before After
Before After

BIN
src/assets/tray/110.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View file

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

Before After
Before After

BIN
src/assets/tray/111.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View file

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

Before After
Before After

109
src/audio.js Normal file
View 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;

View file

@ -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 };

View file

@ -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()
})
})

View file

@ -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
}
}

View file

@ -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);
}
}

View file

@ -1,12 +0,0 @@
const Store = require('electron-store');
const schema = {
token: {
type: 'string',
},
};
export const store = new Store({
schema,
encryptionKey: "super user test"
});

View file

@ -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
};
}

View file

@ -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'),
} }

View file

@ -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
View 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;

View file

@ -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 };

View file

@ -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
});

View file

@ -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();
}

View file

@ -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
View 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());

View file

@ -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();
}

View file

@ -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>

View 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

View file

@ -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 );
};

View 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");

View 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>

View file

@ -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>

View file

@ -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>

View 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;

View file

@ -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>

View 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;

View 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;

View 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;

View file

@ -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";
}
}

View file

@ -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
}
}

View 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;

View file

@ -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>

View 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;

View file

@ -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>

View 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;

View file

@ -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>

View 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;

View file

@ -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>

View 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;

View file

@ -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>

View 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;

View file

@ -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>

View 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;

View file

@ -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>

View 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;

View file

@ -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
}
}

View file

@ -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,
}
}

View file

@ -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
View 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
View file

@ -0,0 +1,2 @@
import App from "./components/app.js";
Vue.createApp(App).mount("#app");

View file

@ -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 = {};
};

View file

@ -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
View 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);
}
}

View file

@ -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
View 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);
}
}
);

View file

@ -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);
}
});
});

View file

@ -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
};

View file

@ -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
}

View file

@ -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
};

View file

@ -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
};

View file

@ -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
};

View file

@ -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
}

View file

@ -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

File diff suppressed because it is too large Load diff

81
src/session.js Normal file
View 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;

View file

@ -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
View 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;

Some files were not shown because too many files have changed in this diff Show more