Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d3f99d6133 | ||
|
|
0c6a58ea24 | ||
|
|
d5fd8b39e7 | ||
|
|
7de0555856 | ||
|
|
ccb07d594b | ||
|
|
41528e7e27 | ||
|
|
92fb472e3c | ||
|
|
f9dcb00e2d | ||
|
|
a4d6a883b0 | ||
|
|
c52ea2bcd2 | ||
|
|
622c705c35 | ||
|
|
e9972bd70a | ||
|
|
6478b1250b | ||
|
|
f96cd7a1bc | ||
|
|
3a89dd82df | ||
|
|
13777f9eed | ||
|
|
1c87ce45bc | ||
|
|
515f67a120 | ||
|
|
95e73f7c28 | ||
|
|
9de4ff0805 | ||
|
|
f1a5cad4db | ||
|
|
2dee81fb36 | ||
|
|
5811f89942 | ||
|
|
29fc9cc000 | ||
|
|
5a4c1a86f6 | ||
|
|
f2c69516df | ||
|
|
fa24a8d46a | ||
|
|
cda3bab739 | ||
|
|
531a32a5a5 | ||
|
|
21687f4b7f | ||
|
|
7283e3f6f9 | ||
|
|
b6b1f3e894 | ||
|
|
a791f973dc | ||
|
|
dd91a9ae9e | ||
|
|
468bb760a4 | ||
|
|
af421e6bfb | ||
|
|
cb40a811fa | ||
|
|
51c4ad8ed7 | ||
|
|
4afa716251 | ||
|
|
e13f90a6cc | ||
|
|
81b4d018fb | ||
|
|
308dbe2b4e | ||
|
|
d00fb0f3be |
38
.eslintrc.js
|
|
@ -1,38 +0,0 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
node: true
|
||||
},
|
||||
extends: [
|
||||
"plugin:vue/vue3-essential",
|
||||
"eslint:recommended",
|
||||
"@vue/typescript/recommended"
|
||||
],
|
||||
parserOptions: {
|
||||
ecmaVersion: 2020
|
||||
},
|
||||
rules: {
|
||||
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
|
||||
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
|
||||
"no-unused-vars": "off",
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"error",
|
||||
{ "argsIgnorePattern": "^_" }
|
||||
]
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: [
|
||||
"**/__tests__/*.{j,t}s?(x)",
|
||||
"**/tests/unit/**/*.spec.{j,t}s?(x)",
|
||||
"*.js"
|
||||
],
|
||||
rules: {
|
||||
"@typescript-eslint/no-var-requires": "off",
|
||||
},
|
||||
env: {
|
||||
jest: true
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
9
.gitignore
vendored
|
|
@ -1,5 +1,5 @@
|
|||
.DS_Store
|
||||
node_modules
|
||||
/node_modules
|
||||
/dist
|
||||
/crate/target/*
|
||||
/.log
|
||||
|
|
@ -28,10 +28,3 @@ pnpm-debug.log*
|
|||
|
||||
#Electron-builder output
|
||||
/dist_electron
|
||||
|
||||
#Electron-builder config
|
||||
electron-builder.yml
|
||||
|
||||
#Window and session states
|
||||
session.json
|
||||
window.json
|
||||
|
|
|
|||
2
.npmrc
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
@crimata:registry=https://gitlab.com/api/v4/projects/28849281/packages/npm/
|
||||
//gitlab.com/api/v4/projects/28849281/packages/npm/:_authToken=${NPM_TOKEN}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
module.exports = {
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset'
|
||||
]
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
module.exports = {
|
||||
preset: '@vue/cli-plugin-unit-jest/presets/typescript-and-babel',
|
||||
transform: {
|
||||
'^.+\\.vue$': 'vue-jest'
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"typeAcquisition": {
|
||||
|
||||
"enable": true
|
||||
}
|
||||
}
|
||||
2213
package-lock.json
generated
Normal file
98
package.json
|
|
@ -1,104 +1,24 @@
|
|||
{
|
||||
"name": "Crimata",
|
||||
"version": "0.9.9",
|
||||
"productName": "crimata-messenger",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "Cross-platform messenger application built with electron, vue3, and TS.",
|
||||
"description": "Cross-platform messenger app (Electron, Vue3, and TS)",
|
||||
"author": {
|
||||
"name": "Enrique Hernandez"
|
||||
"name": "Andrew Gundersen"
|
||||
},
|
||||
"main": "src/main.js",
|
||||
"scripts": {
|
||||
"build": "vue-cli-service electron:build",
|
||||
"dev": "vue-cli-service electron:serve",
|
||||
"postinstall": "electron-builder install-app-deps",
|
||||
"postuninstall": "electron-builder install-app-deps"
|
||||
"start": "electron ."
|
||||
},
|
||||
"main": "init.js",
|
||||
"dependencies": {
|
||||
"@google-cloud/speech": "^4.2.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",
|
||||
"@crimata/nodeaudio": "^0.0.0",
|
||||
"axios": "^0.21.1",
|
||||
"core-js": "^3.6.5",
|
||||
"electron-is-dev": "^2.0.0",
|
||||
"base64-arraybuffer": "^1.0.1",
|
||||
"electron-store": "^8.0.0",
|
||||
"electron-updater": "^4.3.8",
|
||||
"mitt": "^2.1.0",
|
||||
"naudiodon": "^2.3.2",
|
||||
"node-record-lpcm16": "^1.0.1",
|
||||
"update-electron-app": "^2.0.1",
|
||||
"uuid": "^8.3.2",
|
||||
"vue": "^3.0.0-0",
|
||||
"vue-router": "^4.0.0-0",
|
||||
"vuex": "^4.0.0-0",
|
||||
"ws": "^7.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/axios": "^0.14.0",
|
||||
"@types/electron-devtools-installer": "^2.2.0",
|
||||
"@types/jest": "^24.0.19",
|
||||
"@typescript-eslint/eslint-plugin": "^2.33.0",
|
||||
"@typescript-eslint/parser": "^2.33.0",
|
||||
"@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": "^9.0.0",
|
||||
"electron-devtools-installer": "^3.1.0",
|
||||
"electron-log": "^4.3.4",
|
||||
"eslint": "^6.7.2",
|
||||
"eslint-plugin-vue": "^7.0.0-0",
|
||||
"lint-staged": "^9.5.0",
|
||||
"node-sass": "^4.12.0",
|
||||
"optimize-wasm-webpack-plugin": "^1.0.12",
|
||||
"sass-loader": "^8.0.2",
|
||||
"spectron": "11.0.0",
|
||||
"typescript": "~3.9.3",
|
||||
"vue-cli-plugin-electron-builder": "~2.0.0-rc.6",
|
||||
"vue-jest": "^5.0.0-0"
|
||||
},
|
||||
"vue": {
|
||||
"lintOnSave": false,
|
||||
"pluginOptions": {
|
||||
"electronBuilder": {
|
||||
"mainProcessFile": "./src/init.ts",
|
||||
"rendererProcessFile": "./src/render/main.ts",
|
||||
"preload": "./src/render/preload.ts",
|
||||
"builderOptions": {
|
||||
"appId": "com.crimata.ElectronUpdaterApp",
|
||||
"artifactName": "${productName}-${version}.${ext}",
|
||||
"publish": {
|
||||
"provider": "generic",
|
||||
"url": "https://gitlab.com/api/v4/projects/25637892/jobs/artifacts/main/raw/dist_electron?job=build"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"gitHooks": {
|
||||
"pre-commit": "lint-staged"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,jsx,vue,ts,tsx}": [
|
||||
"vue-cli-service lint",
|
||||
"git add"
|
||||
]
|
||||
},
|
||||
"productName": "crimata-messenger",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://gitlab.com/crimata/electron-app.git",
|
||||
"release": "latest"
|
||||
"electron": "^13.2.3"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
44
src/account.js
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
const { ipcMain } = require("electron");
|
||||
|
||||
const store = require("./utils/store");
|
||||
const { postLogin } = require("./api");
|
||||
const { ipcEmit } = require("./utils/emitter");
|
||||
const { launchSession, endSession } = require("./session");
|
||||
|
||||
// store.set("account", "smith12@gmail.com");
|
||||
store.set("account", "adgundersen@gmail.com");
|
||||
|
||||
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;
|
||||
|
|
@ -1,95 +0,0 @@
|
|||
|
||||
import { postAuth, postLogin, postLogout } from "@/api/account";
|
||||
import { endSession, launchSession } from "@/session";
|
||||
import { getToken, setToken, setProfile, getProfile, clearStore } from "./store";
|
||||
import { parseAuthRes } from "./auth";
|
||||
import { ipcEmit } from "@/composables/useEmitter";
|
||||
|
||||
export const accountAuth = async (): Promise<Error | AuthState> => {
|
||||
|
||||
/* attempt to get a login token from the store */
|
||||
const token = getToken();
|
||||
|
||||
/* try to login with it, returns platform secret and new token on success */
|
||||
if (token) {
|
||||
try {
|
||||
|
||||
const res = await postAuth(token);
|
||||
|
||||
const parsed = parseAuthRes(res);
|
||||
|
||||
setToken(parsed.token)
|
||||
setProfile(parsed.profile);
|
||||
|
||||
return {
|
||||
profile: parsed.profile,
|
||||
token: parsed.token
|
||||
};
|
||||
|
||||
} catch(e) {
|
||||
console.log('[ACCOUNT]', e);
|
||||
clearStore();
|
||||
throw(new Error('Failed to authenticate.'));
|
||||
|
||||
}
|
||||
} else {
|
||||
throw(new Error('Unable to authenticate.'));
|
||||
}
|
||||
};
|
||||
|
||||
export const accountLogin: IpcHandlerCallback<AccountCredentials, Profile> = async (payload) => {
|
||||
const account = payload as AccountCredentials;
|
||||
try {
|
||||
|
||||
// attempt login with email password
|
||||
const res = await postLogin(account.email, account.password);
|
||||
const parsed = parseAuthRes(res);
|
||||
|
||||
// save jwt token and profile
|
||||
setToken(parsed.token);
|
||||
setProfile(parsed.profile);
|
||||
|
||||
// launch session
|
||||
launchSession(parsed.token);
|
||||
|
||||
// return profile to renderer
|
||||
return parsed.profile;
|
||||
|
||||
} catch(e) {
|
||||
clearStore();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const accountLogout = async (): Promise<Error | void> => {
|
||||
|
||||
try {
|
||||
// post logout to backend
|
||||
await postLogout();
|
||||
|
||||
// remove key and crimataId
|
||||
clearStore();
|
||||
|
||||
// kill crimata platform session
|
||||
endSession();
|
||||
|
||||
return;
|
||||
|
||||
} catch(e) {
|
||||
console.log('[ACCOUNT]', e);
|
||||
return (new Error('Failed to logout. Please try again.'));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const updateAppState = (): void => {
|
||||
|
||||
const profile = getProfile();
|
||||
|
||||
ipcEmit("set-profile", profile);
|
||||
|
||||
// ipcEmit('messages') etc
|
||||
|
||||
}
|
||||
|
||||
19
src/api.js
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
const axios = require('axios').default;
|
||||
|
||||
const config = require("./config");
|
||||
|
||||
const baseUrl = config.BUSINESS_URL + config.BUSINESS_PREFIX;
|
||||
|
||||
async function postLogin(email, password)
|
||||
{
|
||||
try {
|
||||
const res = await axios.post(baseUrl + "/account/login", {
|
||||
email: email, password: password
|
||||
});
|
||||
return res.data;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
exports.postLogin = postLogin;
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
|
||||
import useHttp from "@/composables/useHttp";
|
||||
import axios from "axios";
|
||||
import {config} from "@/config";
|
||||
|
||||
const { post } = useHttp();
|
||||
|
||||
export const postAuth = async (token: string) => (
|
||||
await axios({
|
||||
url: config.BUSINESS_URL + config.BUSINESS_PREFIX + '/account/authenticate',
|
||||
headers: {
|
||||
Cookie: `crimataCookie=${token}`
|
||||
},
|
||||
method: 'POST',
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
export const postLogin = async (email: string, password: string) => (
|
||||
await post('/account/login', { email, password })
|
||||
);
|
||||
|
||||
|
||||
export const postLogout =
|
||||
async (): Promise<null | Error> => (await post('/account/logout'));
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
BIN
src/assets/icon.icns
Normal file
BIN
src/assets/tray/000.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
src/assets/tray/000@2x.png
Normal file
|
After Width: | Height: | Size: 4 KiB |
BIN
src/assets/tray/001.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
src/assets/tray/001@2x.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
src/assets/tray/010.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
src/assets/tray/010@2x.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
src/assets/tray/011.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
src/assets/tray/011@2x.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
src/assets/tray/100.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
src/assets/tray/100@2x.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
src/assets/tray/101.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
src/assets/tray/101@2x.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
src/assets/tray/110.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
src/assets/tray/110@2x.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
src/assets/tray/111.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
src/assets/tray/111@2x.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
137
src/audio.js
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
const nodeAudio = require("@crimata/nodeaudio");
|
||||
const { globalShortcut, ipcMain } = require("electron");
|
||||
|
||||
const { sendMessage } = require("./io");
|
||||
const { updateTray } = require("./tray");
|
||||
const { encode, decode } = require("./codec");
|
||||
const { backgroundMitt, ipcEmit } = require("./utils/emitter");
|
||||
|
||||
let inputDevice;
|
||||
let outputDevice;
|
||||
|
||||
let setWriteId;
|
||||
let setStreamsId;
|
||||
|
||||
let autoStopId; /* keep track of recording time */
|
||||
|
||||
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.prototype.bind(null, 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 startRecording()
|
||||
{
|
||||
setRecordingStatus(true);
|
||||
|
||||
/* 15s recording time limit */
|
||||
autoStopId = setTimeout(stopRecording, 15000);
|
||||
}
|
||||
|
||||
function stopRecording()
|
||||
{
|
||||
if (autoStopId)
|
||||
{
|
||||
clearTimeout(autoStopId);
|
||||
}
|
||||
|
||||
setRecordingStatus(false);
|
||||
|
||||
sendMessage({
|
||||
category: "audio/L16",
|
||||
text: null,
|
||||
blob: 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() : startRecording();
|
||||
});
|
||||
|
||||
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();
|
||||
|
||||
/* Only terminate PA if initialized */
|
||||
if (setStreamsId)
|
||||
{
|
||||
clearInterval(setStreamsId);
|
||||
nodeAudio.core.Terminate();
|
||||
}
|
||||
}
|
||||
|
||||
function setRecordingStatus(status)
|
||||
{
|
||||
streamState.rec = status;
|
||||
ipcEmit("record", status);
|
||||
updateTray("recording", streamState.rec);
|
||||
}
|
||||
|
||||
function setPlaybackStatus(status)
|
||||
{
|
||||
console.log("Setting playback status: ", status);
|
||||
|
||||
streamState.pb = status;
|
||||
ipcEmit("playback", playbackId, status);
|
||||
updateTray("playback", status);
|
||||
}
|
||||
|
||||
exports.initAudio = initAudio;
|
||||
exports.terminateAudio = terminateAudio;
|
||||
exports.playback = playback;
|
||||
exports.streamState = streamState;
|
||||
|
||||
|
||||
// setWriteId = setTimeout(() => {
|
||||
// setPlaybackStatus(false);
|
||||
// }, 500);
|
||||
187
src/audio.ts
|
|
@ -1,187 +0,0 @@
|
|||
/* eslint @typescript-eslint/no-var-requires: "off" */
|
||||
|
||||
"use strict";
|
||||
|
||||
// where the audio goes
|
||||
let buffer: ArrayBuffer[] = [];
|
||||
|
||||
// place audio data in buffer
|
||||
export const collect: IpcListenerCallback<ArrayBuffer> = (chunk) => {
|
||||
if (chunk) buffer.push(chunk);
|
||||
}
|
||||
|
||||
// return audio and clear buffer
|
||||
export const flush = async () => {
|
||||
const bufferCopy = buffer;
|
||||
buffer = [];
|
||||
return bufferCopy;
|
||||
}
|
||||
|
||||
// import { backgroundMitt } from '@/modules/emitter';
|
||||
// const portAudio = require('naudiodon');
|
||||
|
||||
// // Audio in and out stream objects.
|
||||
// let ai: typeof portAudio.AudioIO | boolean = false;
|
||||
// let ao: typeof portAudio.AudioIO | boolean = false;
|
||||
|
||||
// // Whether activly recording.
|
||||
// let record = false;
|
||||
|
||||
// const audioContainer = {
|
||||
// input: '',
|
||||
// }
|
||||
|
||||
// const audioOptions = {
|
||||
// channelCount: 1,
|
||||
// sampleFormat: 16,
|
||||
// sampleRate: 16000,
|
||||
// deviceId: -1,
|
||||
// closeOnError: false,
|
||||
// }
|
||||
|
||||
// export const toggleRecord = (): void => { record = !record };
|
||||
|
||||
|
||||
// export const fetchAudioInput = (): Promise<Error | string> => (
|
||||
|
||||
// new Promise((resolve, reject) => {
|
||||
|
||||
// try {
|
||||
// resolve(audioContainer.input);
|
||||
// toggleRecord();
|
||||
// } catch (e) {
|
||||
// reject(new Error('Failed to fetch the audio.'))
|
||||
// }
|
||||
|
||||
// })
|
||||
// )
|
||||
|
||||
|
||||
// // Main audio function run by run.ts module.
|
||||
// export function initAudioIO(): void {
|
||||
// console.log("AUDIO:Starting io streams.")
|
||||
|
||||
// if (!ai) {
|
||||
|
||||
// // Initialize and start input stream.
|
||||
// ai = new portAudio.AudioIO({ inOptions: audioOptions });
|
||||
// ai.setEncoding("hex");
|
||||
// ai.start();
|
||||
|
||||
// // On each data chunk...
|
||||
// ai.on('data', (chunk: string) => {
|
||||
|
||||
// // If recording, we capture the data.
|
||||
// if (record) {
|
||||
// console.log('AUDIO:Recording...')
|
||||
// audioContainer.input += chunk;
|
||||
// }
|
||||
|
||||
// // Else, we don't capture and also clear audioContainer.
|
||||
// else {
|
||||
// if (audioContainer.input.length) {
|
||||
// audioContainer.input = "";
|
||||
// }
|
||||
// }
|
||||
|
||||
// });
|
||||
// }
|
||||
|
||||
// if (!ao) {
|
||||
|
||||
// // Initialize and start input stream.
|
||||
// ao = new portAudio.AudioIO({ outOptions: audioOptions });
|
||||
// ao.start();
|
||||
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
// // ---Audio playback--------------------------------------------
|
||||
|
||||
// // Split Buffer into an array of len-sized Buffers.
|
||||
// function bufSplit(buf: Buffer, len: number): Array<Buffer> {
|
||||
// const chunks = [];
|
||||
// let i = 0;
|
||||
// let L = len;
|
||||
|
||||
// while(i < buf.byteLength) {
|
||||
// chunks.push(buf.slice(i, L));
|
||||
// i = L;
|
||||
// L += len;
|
||||
// }
|
||||
|
||||
// return chunks;
|
||||
// }
|
||||
|
||||
// // Audio playback.
|
||||
// export function play(input: string): void {
|
||||
|
||||
// // Format the audio.
|
||||
// const audio = bufSplit(
|
||||
// Buffer.from(input as string, 'hex'),
|
||||
// 8192
|
||||
// );
|
||||
|
||||
// // Called on end of write.
|
||||
// const callback = () => {
|
||||
|
||||
// // We stop audio playback anim.
|
||||
// backgroundMitt.emit('ipc-renderer', {
|
||||
// endpoint: 'stop-playback-anim'
|
||||
// });
|
||||
|
||||
// }
|
||||
|
||||
// write();
|
||||
|
||||
// // Iterate through audio array and write buffers to portAudio writable.
|
||||
// function write() {
|
||||
// let chunk: Buffer;
|
||||
// let ok = true;
|
||||
// let i = 0;
|
||||
|
||||
// do {
|
||||
// chunk = audio[i];
|
||||
// if (i === audio.length - 1) {
|
||||
// // write last chunk.
|
||||
// ao.write(chunk, null, callback);
|
||||
// } else {
|
||||
// // check for backpreassure.
|
||||
// ok = ao.write(chunk, null);
|
||||
// }
|
||||
// i++;
|
||||
// } while (i < audio.length && ok);
|
||||
|
||||
// if (i < audio.length) {
|
||||
// // Had to stop early!
|
||||
// // Write some more once it drains.
|
||||
// ao.once('drain', write);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// // -------------------------------------------------------------
|
||||
|
||||
// // Get's called on window close.
|
||||
// export async function stopStream() {
|
||||
// console.log("AUDIO:Stopping audio stream.")
|
||||
// if (ai) {
|
||||
// try {
|
||||
// await ai.quit()
|
||||
// } catch(e){
|
||||
// console.log('AUDIO: Failed to shutdown audio input.');
|
||||
// throw e;
|
||||
// }
|
||||
// }
|
||||
// if (ao) {
|
||||
// try {
|
||||
// await ao.quit()
|
||||
// } catch(e){
|
||||
// console.log('AUDIO: Failed to shutdown audio output.');
|
||||
// throw e;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
13
src/auth.ts
|
|
@ -1,13 +0,0 @@
|
|||
|
||||
export const parseAuthRes = (authRes: any) => {
|
||||
const token = authRes.headers['set-cookie'][0].split(";")[0].split("=")[1] as string;
|
||||
const profile = authRes.data as Profile;
|
||||
return {
|
||||
token,
|
||||
profile
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
13
src/codec.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
const { ipcMain } = require("electron");
|
||||
const { encode, decode } = require("base64-arraybuffer");
|
||||
|
||||
ipcMain.handle("encode", (_e, arrayBuffer) => {
|
||||
return encode(arrayBuffer);
|
||||
});
|
||||
|
||||
ipcMain.handle("decode", (_e, b64string) => {
|
||||
return decode(b64string);
|
||||
});
|
||||
|
||||
exports.encode = encode;
|
||||
exports.decode = decode;
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
const { autoUpdater } = require('electron-updater');
|
||||
|
||||
let win: boolean;
|
||||
|
||||
// Listen for window creation.
|
||||
backgroundMitt.on('window-active', (state: boolean) => {
|
||||
win = state;
|
||||
});
|
||||
|
||||
// Auto updating.
|
||||
autoUpdater.requestHeaders = { 'PRIVATE-TOKEN': 'mvvgWYwWnot4bisiQMh_' }
|
||||
|
||||
autoUpdater.on('update-available', (info: any) => {
|
||||
console.log(`Update available: ${info.version}`)
|
||||
})
|
||||
|
||||
autoUpdater.on('update-downloaded', (info: any) => {
|
||||
|
||||
const updateDialog = {
|
||||
type: 'info',
|
||||
buttons: ['Restart', 'Later'],
|
||||
title: 'Application Update',
|
||||
message: info.version,
|
||||
detail: 'A new version has been downloaded. Restart the application to apply the updates.'
|
||||
}
|
||||
|
||||
dialog.showMessageBox(updateDialog).then((returnValue) => {
|
||||
if (returnValue.response === 0) autoUpdater.quitAndInstall()
|
||||
})
|
||||
|
||||
})
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
|
||||
// Backend emitter
|
||||
//
|
||||
const EventEmitter = require('events');
|
||||
class BackgroundMitt extends EventEmitter { }
|
||||
|
||||
export const backgroundMitt = new BackgroundMitt();
|
||||
|
||||
export const ipcEmit = <T>(channel: string, payload: T) => {
|
||||
backgroundMitt.emit('ipc-renderer', {
|
||||
channel,
|
||||
payload
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
|
||||
import axios, { AxiosRequestConfig } from 'axios';
|
||||
import {config} from "@/config";
|
||||
|
||||
const baseURL = config.BUSINESS_URL + config.BUSINESS_PREFIX;
|
||||
|
||||
interface Request {
|
||||
endpoint: string;
|
||||
query?: Record<string, any>;
|
||||
config?: Record<string, any>;
|
||||
}
|
||||
|
||||
const makeQuery = (reqQuery: Record<string, any>) => {
|
||||
|
||||
let result = '';
|
||||
|
||||
result = '?' + Object.entries(reqQuery)
|
||||
.map(([ key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
|
||||
.join('&')
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
|
||||
export default function useHttp() {
|
||||
|
||||
const api = axios.create({
|
||||
baseURL,
|
||||
withCredentials: true,
|
||||
});
|
||||
|
||||
|
||||
const post = async (endpoint: string, payload?: Record<string, any>): Promise<any> => (
|
||||
await api.post(endpoint, payload)
|
||||
)
|
||||
|
||||
|
||||
const get = async (req: Request) => {
|
||||
|
||||
if (req.query) {
|
||||
req.endpoint += makeQuery(req.query);
|
||||
}
|
||||
|
||||
const res = await api.get(req.endpoint, req.config);
|
||||
return res;
|
||||
};
|
||||
|
||||
return {
|
||||
get, post
|
||||
}
|
||||
}
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
|
||||
import { ipcMain, IpcMainInvokeEvent, IpcMainEvent } from "electron";
|
||||
|
||||
export class IpcHandler<InputParam, ReturnType> implements IIpcHandler<InputParam, ReturnType> {
|
||||
|
||||
readonly channel: string;
|
||||
|
||||
readonly _handlerCallback: IpcHandlerCallback<InputParam, ReturnType>;
|
||||
|
||||
constructor(options: {
|
||||
channel: string;
|
||||
handlerCallback: IpcHandlerCallback<InputParam, ReturnType>;
|
||||
}) {
|
||||
this.channel = options.channel;
|
||||
this._handlerCallback = options.handlerCallback;
|
||||
}
|
||||
|
||||
handle() {
|
||||
this.remove();
|
||||
ipcMain.handle(this.channel, this._onInvoke);
|
||||
}
|
||||
|
||||
remove() {
|
||||
ipcMain.removeHandler(this.channel);
|
||||
}
|
||||
|
||||
private _onInvoke = (_e: IpcMainInvokeEvent, payload?: string | null): Promise<Error | ReturnType> => {
|
||||
|
||||
return new Promise(async (resolve, reject) => {
|
||||
|
||||
console.log(`[IPC] Handle:${this.channel}`);
|
||||
|
||||
try {
|
||||
|
||||
const params = payload ? JSON.parse(payload) : null;
|
||||
|
||||
const res = await this._handlerCallback(params);
|
||||
|
||||
resolve(res as unknown as ReturnType);
|
||||
|
||||
} catch(e) {
|
||||
console.log(`[IPC] Error:${this.channel}`);
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
export class IpcListener<InputParam> implements IIpcListener<InputParam> {
|
||||
|
||||
readonly channel: string;
|
||||
|
||||
readonly _listenerCallback: IpcListenerCallback<InputParam>;
|
||||
|
||||
constructor(options: {
|
||||
channel: string;
|
||||
listenerCallback: IpcListenerCallback<InputParam>;
|
||||
}) {
|
||||
this.channel = options.channel;
|
||||
this._listenerCallback = options.listenerCallback;
|
||||
}
|
||||
|
||||
listen() {
|
||||
this.remove();
|
||||
ipcMain.on(this.channel, this._onPost);
|
||||
}
|
||||
|
||||
remove() {
|
||||
ipcMain.removeAllListeners(this.channel);
|
||||
}
|
||||
|
||||
private _onPost = (_e: IpcMainEvent, payload?: string | null): void => {
|
||||
|
||||
console.log(`[IPC] Post: ${this.channel}`);
|
||||
|
||||
const params = payload ? JSON.parse(payload) : null;
|
||||
this._listenerCallback(params);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
|
||||
|
||||
|
||||
export default class Canvas {
|
||||
|
||||
messages: Message[];
|
||||
|
||||
/* seed canvas with messages on init */
|
||||
constructor(messages: Message[]) {
|
||||
this.messages = messages;
|
||||
ipcEmit("seed-view", this.messages);
|
||||
}
|
||||
|
||||
/* add a new message to the canvas */
|
||||
add(message: Message) {
|
||||
this.messages.push(message);
|
||||
ipcEmit("update-view", message);
|
||||
}
|
||||
|
||||
/* update an existing message */
|
||||
update(message: Message) {
|
||||
|
||||
/* get the target message */
|
||||
let target_message = this.messages.filter((m: Message) => {
|
||||
return m.uid = message.uid;
|
||||
})[0];
|
||||
|
||||
/* replace the target message */
|
||||
if (target_message) {
|
||||
target_message = message;
|
||||
ipcEmit("update-view", message);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
|
||||
import {config} from "@/config";
|
||||
import fs from 'fs';
|
||||
|
||||
export const saveToJson = (fileName: string, data: any) => {
|
||||
|
||||
fs.writeFile(config.configPath + fileName, JSON.stringify(data), (err) => {
|
||||
if (err) {
|
||||
console.log("Error when saving to json.")
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
|
@ -1,89 +0,0 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
import WebSocket from 'ws';
|
||||
|
||||
|
||||
const _connectionCheckTimeout = 4000;
|
||||
const _reconnectTimeout = 1000;
|
||||
let _connectionCheckInterval: ReturnType<typeof setTimeout>;
|
||||
|
||||
|
||||
export default function useWebSockets(
|
||||
messageCallback: (message: string) => void,
|
||||
connectionStatusCallback: (alive: boolean) => void,
|
||||
) {
|
||||
|
||||
let socket: WebSocket;
|
||||
|
||||
const send = async (data: Record<string, any>): Promise<boolean> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (socket) {
|
||||
if (socket.readyState === WebSocket.OPEN) {
|
||||
socket.send(JSON.stringify(data));
|
||||
resolve(true);
|
||||
}
|
||||
}
|
||||
reject(false);
|
||||
});
|
||||
}
|
||||
|
||||
const connect = (socketUrl: string, secret: string) => {
|
||||
|
||||
// avoid setting multiple interval;
|
||||
if (_connectionCheckInterval) clearInterval(_connectionCheckInterval);
|
||||
|
||||
/* create a new socket */
|
||||
socket = new WebSocket(socketUrl);
|
||||
|
||||
/* add event listeners */
|
||||
socket.on("open", () => {
|
||||
|
||||
socket.send(JSON.stringify({key: secret}));
|
||||
|
||||
// ping server
|
||||
_connectionCheckInterval = setInterval(() => {
|
||||
|
||||
socket.ping(null, true, (e: Error) => {
|
||||
if (e) {
|
||||
socket.close();
|
||||
connectionStatusCallback(false);
|
||||
setTimeout(() => connect(socketUrl, secret), _reconnectTimeout);
|
||||
}
|
||||
});
|
||||
|
||||
}, _connectionCheckTimeout);
|
||||
|
||||
});
|
||||
|
||||
socket.on("message", (event: WebSocket.MessageEvent) => {
|
||||
console.log("message received", event);
|
||||
messageCallback(event.toString())
|
||||
});
|
||||
|
||||
socket.on("close", (event: WebSocket.CloseEvent) => {
|
||||
connectionStatusCallback(false);
|
||||
clearInterval(_connectionCheckInterval);
|
||||
if (!event.wasClean) {
|
||||
setTimeout(() => connect(socketUrl, secret), _reconnectTimeout);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
socket.on("pong", () => connectionStatusCallback(true));
|
||||
|
||||
}
|
||||
|
||||
const close = () => {
|
||||
if (socket) {
|
||||
socket.close();
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
connect,
|
||||
send,
|
||||
close
|
||||
};
|
||||
|
||||
}
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
|
||||
import { app } from "electron";
|
||||
const { app } = require("electron");
|
||||
|
||||
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_IP = env.BUSINESS_IP || 'http://127.0.0.1';
|
||||
|
||||
export const config = {
|
||||
module.exports = {
|
||||
PLATFORM_URL: `${PLATFORM_IP}:${PLATFORM_PORT}`,
|
||||
BUSINESS_URL: `${BUSINESS_IP}:${BUSINESS_PORT}`,
|
||||
BUSINESS_PREFIX: '/api',
|
||||
configPath: app.getPath('userData')
|
||||
}
|
||||
configPath: app.getPath('userData'),
|
||||
}
|
||||
52
src/init.ts
|
|
@ -1,52 +0,0 @@
|
|||
/**
|
||||
* Entry point for Crimata electron app.
|
||||
* "Look on my Works, ye Mighty, and despair!"
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
import { app, protocol } from "electron";
|
||||
import createWindow from "./window";
|
||||
import main from "./main";
|
||||
import { backgroundMitt } from '@/composables/useEmitter';
|
||||
|
||||
console.log('Starting Crimata electron app.');
|
||||
|
||||
// Scheme must be registered before the app is ready
|
||||
protocol.registerSchemesAsPrivileged([
|
||||
{ scheme: "app", privileges: { secure: true, standard: true } }
|
||||
]);
|
||||
|
||||
const isDev = require('electron-is-dev');
|
||||
|
||||
let win: boolean;
|
||||
|
||||
// Listen for window creation.
|
||||
backgroundMitt.on('window-active', (state: boolean) => {
|
||||
win = state;
|
||||
});
|
||||
|
||||
/* Start main process on ready */
|
||||
app.on("ready", async () => {
|
||||
await main();
|
||||
});
|
||||
|
||||
// Must keep to ensure app doesn't quit on close.
|
||||
app.on("before-quit", async () => {
|
||||
});
|
||||
|
||||
// Must keep to ensure app doesn't quit on close.
|
||||
app.on("window-all-closed", () => {
|
||||
});
|
||||
|
||||
// When user clicks app icon (re-open)
|
||||
app.on("activate", () => {
|
||||
if (!win) createWindow();
|
||||
});
|
||||
|
||||
// Exit cleanly on request from parent process in development mode.
|
||||
if (isDev) {
|
||||
process.on("SIGTERM", () => {
|
||||
app.quit();
|
||||
});
|
||||
}
|
||||
80
src/io.js
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
const WebSocket = require("ws");
|
||||
const { ipcMain } = require("electron");
|
||||
|
||||
const config = require("./config");
|
||||
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("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;
|
||||
|
||||
|
||||
|
||||
/* ------------- Test Messages------------- */
|
||||
|
||||
|
||||
|
||||
setTimeout(() => {
|
||||
|
||||
// sendMessage({
|
||||
// category: "text/plain",
|
||||
// text: "show contacts",
|
||||
// blob: null
|
||||
// })
|
||||
|
||||
sendMessage({
|
||||
category: "text/plain",
|
||||
text: "add contact John Smith, john.smith97@gmail.com",
|
||||
blob: null
|
||||
})
|
||||
|
||||
}, 2000);
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
import { accountLogin, accountLogout, accountProfile } from "@/account";
|
||||
import { IpcHandler } from "@/composables/useIpcMain";
|
||||
import { flush } from "@/audio";
|
||||
|
||||
|
||||
const LOGIN_CHANNEL = "invoke-account-login";
|
||||
const LOGOUT_CHANNEL = "invoke-account-logout";
|
||||
const GET_AUDIO_CHANNEL = "invoke-audio-flush";
|
||||
|
||||
export const loginHandler = new IpcHandler({
|
||||
channel: LOGIN_CHANNEL,
|
||||
handlerCallback: accountLogin
|
||||
});
|
||||
|
||||
export const logoutHandler = new IpcHandler({
|
||||
channel: LOGOUT_CHANNEL,
|
||||
handlerCallback: accountLogout
|
||||
});
|
||||
|
||||
export const getAudioHandler = new IpcHandler({
|
||||
channel: GET_AUDIO_CHANNEL,
|
||||
handlerCallback: flush
|
||||
});
|
||||
|
||||
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
import * as handlers from "./handlers";
|
||||
import * as listeners from "./listeners";
|
||||
|
||||
const ipcHandlers: IPCHandlers = {};
|
||||
const ipcListeners: IPCListeners = {};
|
||||
|
||||
const _initHandlers = (): void => {
|
||||
for (const [key, handler] of Object.entries(handlers)) {
|
||||
if (!(key in ipcHandlers)) {
|
||||
ipcHandlers[key] = handler;
|
||||
handler.handle();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const _initListeners = (): void => {
|
||||
for (const [key, listener] of Object.entries(listeners)) {
|
||||
if (!(key in ipcListeners)) {
|
||||
ipcListeners[key] = listener;
|
||||
listener.listen();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default function initIpcMain(): void {
|
||||
_initHandlers();
|
||||
_initListeners();
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
|
||||
import { IpcListener } from "@/composables/useIpcMain"
|
||||
import { sendMessage } from '@/session';
|
||||
import { collect } from "@/audio";
|
||||
import { updateAppState } from "@/account";
|
||||
|
||||
const CLIENT_MESSAGE_CHANNEL = "post-session-send"
|
||||
const GET_AUDIO_CHANNEL = "post-audio-collect";
|
||||
const APP_MOUNT_CHANNEL = "post-app-mount";
|
||||
|
||||
export const messageListener = new IpcListener<Message>({
|
||||
channel: CLIENT_MESSAGE_CHANNEL,
|
||||
listenerCallback: sendMessage
|
||||
});
|
||||
|
||||
export const audioChunkListener = new IpcListener<ArrayBuffer>({
|
||||
channel: GET_AUDIO_CHANNEL,
|
||||
listenerCallback: collect
|
||||
});
|
||||
|
||||
export const appMountListener = new IpcListener<null>({
|
||||
channel: APP_MOUNT_CHANNEL,
|
||||
listenerCallback: updateAppState
|
||||
});
|
||||
29
src/main.js
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
const { app, protocol, ipcMain, nativeTheme } = 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");
|
||||
|
||||
// Assert a light theme
|
||||
nativeTheme.themeSource = "light";
|
||||
|
||||
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());
|
||||
39
src/main.ts
|
|
@ -1,39 +0,0 @@
|
|||
/**
|
||||
* Where the background logic really begins, gets called by app.onReady().
|
||||
*
|
||||
* Handles authentication. If profile is set, we launch a session, which consis
|
||||
* of opening a connection with the platform, initializing the audio streams.
|
||||
*
|
||||
* The session is primarily an interface between the frontend and the platform,
|
||||
* relaying messages from one to the other.
|
||||
*
|
||||
*/
|
||||
|
||||
import initIpcMain from "@/ipc/index";
|
||||
import { accountAuth, updateAppState } from "./account";
|
||||
import { launchSession } from "./session";
|
||||
import createWindow from "./window";
|
||||
|
||||
let authState: AuthState | null;
|
||||
|
||||
export default async function main() {
|
||||
|
||||
/* initiate controls for frontend to use when needed */
|
||||
initIpcMain();
|
||||
|
||||
/* launch browser window */
|
||||
await createWindow();
|
||||
|
||||
try {
|
||||
authState = await accountAuth() as AuthState;
|
||||
} catch(e) {
|
||||
console.log('AUTH:', e);
|
||||
authState = null;
|
||||
} finally {
|
||||
if (authState) {
|
||||
launchSession(authState.token as string);
|
||||
}
|
||||
updateAppState();
|
||||
}
|
||||
|
||||
}
|
||||
52
src/preview.html
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: "SF Pro Text";
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#titlebar {
|
||||
position: fixed;
|
||||
width: 100vw;
|
||||
height: 54px;
|
||||
opacity: 0;
|
||||
-webkit-app-region: drag;
|
||||
border: none;
|
||||
outline: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.building {
|
||||
width: 250px;
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
.email {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.email-subject {
|
||||
font-size: 16px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<button id="titlebar"></button>
|
||||
|
||||
<div id="content"></div>
|
||||
|
||||
<script>
|
||||
window.mainApi.on("preview-init", (content) => {
|
||||
document.getElementById("content").innerHTML = content;
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,76 +0,0 @@
|
|||
<template>
|
||||
<!-- Only render when profile has been set -->
|
||||
<main id="app" v-if="authComplete">
|
||||
|
||||
<Header />
|
||||
|
||||
<Messenger
|
||||
v-if="profile"
|
||||
/>
|
||||
|
||||
<!-- Main Components
|
||||
-->
|
||||
<Login v-else />
|
||||
|
||||
</main>
|
||||
|
||||
<!-- Show spash screen if ready is false -->
|
||||
<Splash v-else />
|
||||
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
import { defineComponent, onMounted } from "vue";
|
||||
import { IpcRendererEvent } from "electron";
|
||||
|
||||
import Splash from "@/render/components/splash.vue";
|
||||
import Messenger from "@/render/components/messenger.vue";
|
||||
import Login from "@/render/components/login.vue";
|
||||
import Header from "@/render/components/header.vue";
|
||||
|
||||
import { profile, authComplete } from "@/render/composables/useProfile";
|
||||
import { postAppMount } from "@/render/ipc";
|
||||
|
||||
export default defineComponent({
|
||||
|
||||
components: {
|
||||
Splash,
|
||||
Messenger,
|
||||
Login,
|
||||
Header
|
||||
},
|
||||
|
||||
setup() {
|
||||
|
||||
onMounted(() => postAppMount());
|
||||
|
||||
return {
|
||||
authComplete,
|
||||
profile
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#app {
|
||||
font-family: "SF Pro Text";
|
||||
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
</style>
|
||||
14
src/render/assets/connectLogo.svg
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24.87" height="23.001" viewBox="0 0 24.87 23.001">
|
||||
<g id="Group_541" data-name="Group 541" transform="translate(-4127 -507)">
|
||||
<g id="Group_33" data-name="Group 33" transform="translate(4127 507)">
|
||||
<g id="Group_32" data-name="Group 32" transform="translate(0 0)">
|
||||
<g id="Group_31" data-name="Group 31" transform="translate(0 11.948)">
|
||||
<circle id="Ellipse_50" data-name="Ellipse 50" cx="5.527" cy="5.527" r="5.527" transform="translate(0 0)" fill="#383838"/>
|
||||
<circle id="Ellipse_51" data-name="Ellipse 51" cx="5.527" cy="5.527" r="5.527" transform="translate(13.817 0)" fill="#383838"/>
|
||||
</g>
|
||||
<circle id="Ellipse_52" data-name="Ellipse 52" cx="5.527" cy="5.527" r="5.527" transform="translate(6.898 0)" fill="#383838"/>
|
||||
<path id="Path_150" data-name="Path 150" d="M36.27,83.67" transform="translate(-30.733 -66.196)" fill="#ff0"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 973 B |
7
src/render/assets/settingsIcon.svg
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="25" height="5" viewBox="0 0 25 5">
|
||||
<g id="Group_611" data-name="Group 611" transform="translate(-4032 499)">
|
||||
<circle id="Ellipse_71" data-name="Ellipse 71" cx="2.5" cy="2.5" r="2.5" transform="translate(4032 -499)" fill="#9b9b9b"/>
|
||||
<circle id="Ellipse_72" data-name="Ellipse 72" cx="2.5" cy="2.5" r="2.5" transform="translate(4042 -499)" fill="#9b9b9b"/>
|
||||
<circle id="Ellipse_73" data-name="Ellipse 73" cx="2.5" cy="2.5" r="2.5" transform="translate(4052 -499)" fill="#9b9b9b"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 553 B |
42
src/render/components/app.js
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import Splash from "./splash.js";
|
||||
import Messenger from "./messenger.js";
|
||||
import Login from "./login.js";
|
||||
|
||||
const account = Vue.ref(null);
|
||||
|
||||
const App =
|
||||
{
|
||||
components: {
|
||||
Splash,
|
||||
Messenger,
|
||||
Login
|
||||
},
|
||||
|
||||
setup()
|
||||
{
|
||||
return { account };
|
||||
},
|
||||
|
||||
template: `
|
||||
<main id="app" v-if="account !== null">
|
||||
|
||||
<button id="header-titlebar" />
|
||||
|
||||
<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");
|
||||
36
src/render/components/bubble.js
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
const Bubble =
|
||||
{
|
||||
props: ["category", "text", "blob", "modifier", "child"],
|
||||
|
||||
setup(props)
|
||||
{
|
||||
const getUrl = () => {
|
||||
return `data:${props.category};base64,${props.blob}`;
|
||||
}
|
||||
|
||||
Vue.onMounted(() => {
|
||||
document.getElementById("messenger").dispatchEvent(new CustomEvent('adjust-scroll'));
|
||||
});
|
||||
|
||||
return { getUrl };
|
||||
},
|
||||
|
||||
template: `
|
||||
<div :class="'bubble ' + modifier + '-bubble ' + modifier +'-'+ child">
|
||||
|
||||
<p v-if="category=='text/plain'">{{ text }}</p>
|
||||
|
||||
<p v-else-if="category=='audio/L16'">{{ text }}</p>
|
||||
|
||||
<div v-else-if="category=='text/html'" v-html="text"></div>
|
||||
|
||||
<img v-else-if="category.startsWith('image/')" :src="getUrl()" />
|
||||
|
||||
<a v-else :href="getUrl()">{{ text }}</a>
|
||||
|
||||
</div>`
|
||||
};
|
||||
|
||||
export default Bubble;
|
||||
|
||||
// TODO: should include other styling/actions for audio bubbles
|
||||
|
|
@ -1,331 +0,0 @@
|
|||
<template>
|
||||
<div :class="`${type}-message`">
|
||||
|
||||
<!-- message bubble -->
|
||||
<div :class="`${type}-${child}-bubble`">
|
||||
<div class="notification-dot"/>
|
||||
<div class="error-dot"/>
|
||||
{{ text }}
|
||||
</div>
|
||||
|
||||
<!-- message context -->
|
||||
<span class="context">
|
||||
<div :class="`${type}-icon`"/>
|
||||
{{ context }}
|
||||
</span>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang='ts'>
|
||||
|
||||
import { defineComponent, ref, onMounted } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: "Bubble",
|
||||
|
||||
props: ["text", "context", "type", "position"],
|
||||
|
||||
setup() {
|
||||
|
||||
const seen = ref(false);
|
||||
/* initialize seen state */
|
||||
if (document.visibilityState === "visible") {
|
||||
seen.value = true;
|
||||
} else seen.value = false;
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
if(seen.value)
|
||||
document.addEventListener("visibilitychange", () => {
|
||||
seen.value = true
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
return {
|
||||
seen
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
.message {
|
||||
width: 100vw;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-top: 9px;
|
||||
padding-bottom: 9px;
|
||||
}
|
||||
|
||||
.message:first-child {
|
||||
margin-top: 55px;
|
||||
}
|
||||
|
||||
.message:last-child {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.sf-message {
|
||||
@extend .message;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.ai-message, .fr-message {
|
||||
@extend .message;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.bubble {
|
||||
position: relative;
|
||||
max-width: 66vw;
|
||||
font-family: "SF Pro Text";
|
||||
font-size: 14px;
|
||||
padding: 10px;
|
||||
border-radius: 18px;
|
||||
}
|
||||
|
||||
.sf-bubble {
|
||||
@extend .bubble;
|
||||
background-color: #58c4fd;
|
||||
color: white;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
.firstChildMessage {
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
.middleChildMessage {
|
||||
padding-top: 2px;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
.lastChildMessage {
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
#aiMessage {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
#sfMessage {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
#frMessage {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.messageBox {
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
// Animate on render.
|
||||
animation-name: appear;
|
||||
animation-duration: 0.25s;
|
||||
}
|
||||
|
||||
#aiMessageBox {
|
||||
margin-left: 20px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
#sfMessageBox {
|
||||
margin-right: 20px;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
#frMessageBox {
|
||||
margin-left: 20px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.bubble {
|
||||
position: relative;
|
||||
|
||||
max-width: 66vw;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
font-family: "SF Pro Text";
|
||||
font-size: 14px;
|
||||
|
||||
padding: 10px;
|
||||
|
||||
border-radius: 18px;
|
||||
}
|
||||
|
||||
@keyframes appear {
|
||||
10% {
|
||||
transform: scale(0.3);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
#aiBubble {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
#sfBubble {
|
||||
background-color: #58c4fd;
|
||||
color: white;
|
||||
}
|
||||
|
||||
#frBubble {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.sf-firstChildBubble {
|
||||
border-bottom-right-radius: 9px;
|
||||
}
|
||||
|
||||
.sf-middleChildBubble {
|
||||
border-top-right-radius: 9px;
|
||||
border-bottom-right-radius: 9px;
|
||||
}
|
||||
|
||||
.sf-lastChildBubble {
|
||||
border-top-right-radius: 9px;
|
||||
}
|
||||
|
||||
.ai-firstChildBubble, .fr-firstChildBubble {
|
||||
border-bottom-left-radius: 9px;
|
||||
}
|
||||
|
||||
.ai-middleChildBubble, .fr-middleChildBubble {
|
||||
border-top-left-radius: 9px;
|
||||
border-bottom-left-radius: 9px;
|
||||
}
|
||||
|
||||
.ai-lastChildBubble, .fr-lastChildBubble {
|
||||
border-top-left-radius: 9px;
|
||||
}
|
||||
|
||||
.notify {
|
||||
position: absolute;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background-color: #58D9FF;
|
||||
top: -5px;
|
||||
left: -5px;
|
||||
border: 2px solid #EBEBEB;
|
||||
transform: scale(0);
|
||||
|
||||
animation-name: notify-anim;
|
||||
animation-duration: 5s;
|
||||
}
|
||||
|
||||
@keyframes notify-anim {
|
||||
0%, 90% {
|
||||
transform: scale(1);
|
||||
}
|
||||
100% {
|
||||
transform: scale(0);
|
||||
}
|
||||
}
|
||||
|
||||
.context {
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
font-family: "SF Compact Display";
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.photo {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
margin-right: 5px;
|
||||
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
|
||||
font-size: 14px;
|
||||
|
||||
background-color: white;
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
// Apply for audio message playback.
|
||||
.playing {
|
||||
animation-name: circle1;
|
||||
animation-duration: 2s;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
@keyframes circle1 {
|
||||
0%,
|
||||
100% {
|
||||
box-shadow: 0 0 0 0.4em rgba(195, 195, 195, 0.4), 0 0 0 0.25em rgba(195, 195, 195, 0.15);
|
||||
}
|
||||
25% {
|
||||
box-shadow: 0 0 0 0.15em rgba(195, 195, 195, 0.15), 0 0 0 0.4em rgba(195, 195, 195, 0.3);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 0 0.55em rgba(195, 195, 195, 0.55), 0 0 0 0.15em rgba(195, 195, 195, 0.05);
|
||||
}
|
||||
75% {
|
||||
box-shadow: 0 0 0 0.25em rgba(195, 195, 195, 0.25), 0 0 0 0.55em rgba(195, 195, 195, 0.45);
|
||||
}
|
||||
}
|
||||
|
||||
.contentLoader {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.contentLoaderDot {
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
|
||||
margin: 2px;
|
||||
border-radius: 2.5px;
|
||||
background-color: #357CA2;
|
||||
}
|
||||
|
||||
.questionMark {
|
||||
font-weight: 900;
|
||||
color: #357CA2;
|
||||
}
|
||||
|
||||
// .divider {
|
||||
// width: 100vw;
|
||||
// display: flex;
|
||||
// justify-content: center;
|
||||
// align-items: center;
|
||||
|
||||
// font-family: "SF Compact Display";
|
||||
// font-size: 12px;
|
||||
// font-weight: bold;
|
||||
// color: #9B9B9B;
|
||||
|
||||
// margin-bottom: 18px;
|
||||
// }
|
||||
|
||||
|
||||
</style>
|
||||
|
||||
|
||||
38
src/render/components/context.js
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
|
||||
const Context =
|
||||
{
|
||||
props: ["modifier", "context", "avatar", "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-show="avatar"
|
||||
class="context-avatar"
|
||||
:class="{ audio: playback }"
|
||||
>
|
||||
|
||||
<img :src="'data:image/png;base64,' + avatar" />
|
||||
|
||||
</div>
|
||||
|
||||
{{ context }}
|
||||
|
||||
</span> `
|
||||
}
|
||||
|
||||
export default Context;
|
||||
198
src/render/components/control/draggify.js
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
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();
|
||||
|
||||
window.removeEventListener('mousemove', onMouseMove);
|
||||
window.removeEventListener('mouseup', onMouseUp);
|
||||
|
||||
// See if and calculate reposition.
|
||||
let x = 0; // vector change
|
||||
let y = 0;
|
||||
|
||||
if (elementX.value < 0) {
|
||||
x = (elementX.value - margin)*-1
|
||||
}
|
||||
|
||||
if (elementX.value > winW - elementLength) {
|
||||
const b = winW - elementLength - margin;
|
||||
x = (elementX.value - b)*-1
|
||||
}
|
||||
|
||||
// Reposition y
|
||||
if (elementY.value < 0) {
|
||||
y = (elementY.value - margin)*-1
|
||||
}
|
||||
|
||||
if (elementY.value > winH - elementLength) {
|
||||
const d = winH - elementLength - margin;
|
||||
y = (elementY.value - d)*-1
|
||||
}
|
||||
|
||||
// Reposition if needed.
|
||||
if (x !== 0 || y !== 0) repositionAnime(x, y);
|
||||
|
||||
xShort = calcSideProximity(elementX.value, elementLength, winW);
|
||||
yShort = calcSideProximity(elementY.value, elementLength, winH);
|
||||
|
||||
// Update percentages.
|
||||
percentX = elementX.value / winW;
|
||||
percentY = elementY.value / winH;
|
||||
|
||||
// Save position.
|
||||
savePosition();
|
||||
}
|
||||
|
||||
// Update position of targetEl on windowResize.
|
||||
const onWindowResize = (_e) => {
|
||||
|
||||
// Update window dimensions.
|
||||
winW = window.innerWidth;
|
||||
winH = window.innerHeight;
|
||||
|
||||
elementX.value = calcPosition(elementX.value, elementLength, percentX, xShort, winW);
|
||||
elementY.value = calcPosition(elementY.value, elementLength, percentY, yShort, winH);
|
||||
|
||||
savePosition();
|
||||
}
|
||||
|
||||
const savePosition = () => {
|
||||
window.localStorage.setItem(saveLocation, JSON.stringify({
|
||||
x: elementX.value,
|
||||
y: elementY.value
|
||||
}));
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------
|
||||
|
||||
Vue.onMounted(() => {
|
||||
|
||||
element = document.getElementById(elementId);
|
||||
|
||||
elementLength = element.offsetWidth;
|
||||
|
||||
// Initialize the positional references.
|
||||
percentX = elementX.value / window.innerWidth;
|
||||
percentY = elementY.value / window.innerHeight;
|
||||
|
||||
xShort = calcSideProximity(elementX.value, elementLength, window.innerWidth);
|
||||
yShort = calcSideProximity(elementX.value, elementLength, window.innerHeight);
|
||||
|
||||
// Then, we can listen for window resize (and mousedown).
|
||||
element.addEventListener("mousedown", onMouseDown);
|
||||
window.addEventListener('resize', onWindowResize)
|
||||
});
|
||||
|
||||
// remove event listeners on component dismount.
|
||||
Vue.onUnmounted(() => {
|
||||
element.removeEventListener('mousedown', onMouseDown);
|
||||
window.removeEventListener('resize', onWindowResize)
|
||||
window.removeEventListener('mouseup', onMouseUp)
|
||||
window.removeEventListener('mousemove', onMouseMove);
|
||||
});
|
||||
|
||||
// Try loading initPosition, otherwise set default values
|
||||
let initPosition;
|
||||
const rawData = window.localStorage.getItem("saveLocation")
|
||||
rawData ? initPosition = JSON.parse(rawData) : initPosition = defaultPosition;
|
||||
|
||||
elementX.value = initPosition.x;
|
||||
elementY.value = initPosition.y;
|
||||
|
||||
return { elementX, elementY };
|
||||
|
||||
}
|
||||
|
||||
export default draggify;
|
||||
24
src/render/components/control/scroll.js
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
let el;
|
||||
|
||||
const initScroll = (elementId) => {
|
||||
el = document.getElementById(elementId);
|
||||
el.addEventListener('adjust-scroll', adjustScroll);
|
||||
window.addEventListener('resize', adjustScroll);
|
||||
}
|
||||
|
||||
const isBottom = () => {
|
||||
if (el) {
|
||||
return el.scrollHeight - el.clientHeight <= el.scrollTop + 1;
|
||||
}
|
||||
}
|
||||
|
||||
const adjustScroll = () => {
|
||||
if (el) {
|
||||
el.scrollTo({
|
||||
top: el.scrollHeight - el.clientHeight,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default initScroll;
|
||||
76
src/render/components/control/text.js
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
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", {
|
||||
category: "text/plain",
|
||||
text: el.value,
|
||||
blob: null
|
||||
});
|
||||
show.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
Vue.watch(left, calcSide);
|
||||
|
||||
Vue.watch(show, (c, _p) => {
|
||||
if (c) {
|
||||
el.focus();
|
||||
} else {
|
||||
el.value = "";
|
||||
el.blur();
|
||||
}
|
||||
});
|
||||
|
||||
calcSide();
|
||||
|
||||
return { leftSide, show };
|
||||
}
|
||||
|
||||
export default useText;
|
||||
|
|
@ -1,102 +0,0 @@
|
|||
import { ref } from "vue";
|
||||
import anime from "animejs";
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
|
||||
export function animateTextInput () {
|
||||
|
||||
const side = ref("right");
|
||||
|
||||
function show () {
|
||||
const t1 = (side.value === "right") ? 50 : -70;
|
||||
const t2 = (side.value === "right") ? 110 : -130;
|
||||
|
||||
anime({
|
||||
targets: '#textInput',
|
||||
opacity: [0, 1],
|
||||
translateX: [t1, t2],
|
||||
scale: [0.3, 1],
|
||||
duration: 500,
|
||||
easing: 'easeOutExpo',
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
function hide () {
|
||||
const t = (side.value === "right") ? 50 : -80;
|
||||
|
||||
anime({
|
||||
targets: '#textInput',
|
||||
opacity: [1, 0],
|
||||
translateX: t,
|
||||
scale: 0.3,
|
||||
duration: 500,
|
||||
easing: 'easeOutExpo',
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
function switchSide () {
|
||||
const t = (side.value === "right") ? -130 : 110;
|
||||
|
||||
anime({
|
||||
targets: '#textInput',
|
||||
translateX: t,
|
||||
duration: 500,
|
||||
easing: 'easeOutExpo',
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
return {
|
||||
side,
|
||||
show,
|
||||
hide,
|
||||
switchSide
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
export function animateAudioInput () {
|
||||
|
||||
function show () {
|
||||
anime({
|
||||
targets: '#recIcon',
|
||||
opacity: [0, 0.75],
|
||||
scale: [0.0, 1],
|
||||
duration: 250,
|
||||
easing: 'linear',
|
||||
})
|
||||
}
|
||||
|
||||
function hide () {
|
||||
anime({
|
||||
targets: '#recIcon',
|
||||
opacity: [0.75, 0],
|
||||
scale: [1, 0],
|
||||
duration: 250,
|
||||
easing: 'linear',
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
show,
|
||||
hide
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
|
||||
export function newMessage ({
|
||||
text=false,
|
||||
audio=false,
|
||||
context=false,
|
||||
uid=uuidv4()
|
||||
}) {
|
||||
return {
|
||||
text: text,
|
||||
audio: audio,
|
||||
context: context,
|
||||
uid: uid
|
||||
};
|
||||
}
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
import { onMounted, onUnmounted, ref, Ref } from "vue";
|
||||
import { postMessage } from "@/render/ipc";
|
||||
import { newMessage, animateAudioInput } from "./helpers";
|
||||
import { invokeReturnAudio, postAudioChunk } from "@/render/ipc";
|
||||
|
||||
export default function useAudioInputController (typing: Ref) {
|
||||
|
||||
const recording = ref(false);
|
||||
let mediaRecorder: MediaRecorder;
|
||||
|
||||
const { show, hide } = animateAudioInput();
|
||||
|
||||
// initialize audio
|
||||
const conf = {audio: true, video: false}
|
||||
navigator.mediaDevices.getUserMedia(conf).then((stream: MediaStream) => {
|
||||
|
||||
const options = {mimeType: 'audio/webm'};
|
||||
mediaRecorder = new MediaRecorder(stream, options);
|
||||
|
||||
// post any mew audio to backend
|
||||
mediaRecorder.addEventListener('dataavailable', (e: BlobEvent) => {
|
||||
e.data.arrayBuffer().then((buff: ArrayBuffer) => {
|
||||
postAudioChunk(buff);
|
||||
});
|
||||
});
|
||||
|
||||
// get audio and post new message to backend
|
||||
mediaRecorder.addEventListener('stop', (_e: Event) => {
|
||||
invokeReturnAudio().then((audio: ArrayBuffer[] | Error) => {
|
||||
console.log(audio);
|
||||
// postMessage(newMessage({audio: audio}));
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
// start recording on space bar
|
||||
const record = () => {
|
||||
console.log("INPT:Capturing audio...")
|
||||
mediaRecorder.start();
|
||||
recording.value = true;
|
||||
show()
|
||||
}
|
||||
|
||||
// stop recording and send on release
|
||||
const stop = () => {
|
||||
console.log("INPT:Stopping record.")
|
||||
mediaRecorder.stop();
|
||||
recording.value = false;
|
||||
hide();
|
||||
}
|
||||
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.keyCode == 32 && !typing.value) record();
|
||||
}
|
||||
|
||||
const onKeyUp = (e: KeyboardEvent) => {
|
||||
if (e.keyCode == 32 && recording.value) stop();
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener("keydown", onKeyDown);
|
||||
window.addEventListener("keyup", onKeyUp);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener("keydown", onKeyDown);
|
||||
window.removeEventListener("keyup", onKeyUp);
|
||||
})
|
||||
|
||||
return {
|
||||
recording
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,134 +0,0 @@
|
|||
import { Ref, ref, watch, onMounted, onUnmounted } from "vue";
|
||||
import { postMessage } from "@/render/ipc";
|
||||
import { newMessage, animateTextInput } from "./helpers";
|
||||
|
||||
|
||||
export default function useTextInputController(elementX: Ref) {
|
||||
|
||||
let textInput: HTMLInputElement | null;
|
||||
|
||||
const { side, show, hide, switchSide } = animateTextInput();
|
||||
|
||||
let firstKey = true;
|
||||
const typing = ref(false);
|
||||
|
||||
// Prep inputItem for typing.
|
||||
const prepInput = () => {
|
||||
show()
|
||||
typing.value = true
|
||||
}
|
||||
|
||||
// Clear and hide inputItem after done typing.
|
||||
const clearInput = () => {
|
||||
|
||||
if (textInput) {
|
||||
textInput.value = "";
|
||||
textInput.blur();
|
||||
}
|
||||
|
||||
hide()
|
||||
firstKey = true;
|
||||
typing.value = false;
|
||||
}
|
||||
|
||||
// Send a message and clean up after.
|
||||
const sendMessage = () => {
|
||||
if (textInput) {
|
||||
|
||||
// Send it to the backend for processing.
|
||||
const message = newMessage({
|
||||
text: false
|
||||
});
|
||||
|
||||
// postMessage(message);
|
||||
|
||||
clearInput()
|
||||
}
|
||||
}
|
||||
|
||||
// Keys that are capable of opening the text input (numbers and letters).
|
||||
const isHotKey = (key: number) => {
|
||||
if (key >= 47 && key <= 91) { // a letter
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
//---Callbacks-----------------------------------------------
|
||||
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
const key = e.keyCode;
|
||||
|
||||
if (textInput) {
|
||||
|
||||
// Only runs on firstKey.
|
||||
if (firstKey) {
|
||||
|
||||
if (!isHotKey(key)) {
|
||||
return
|
||||
}
|
||||
|
||||
prepInput()
|
||||
}
|
||||
|
||||
textInput.focus();
|
||||
|
||||
// Close input when no text or on ESC.
|
||||
if ((textInput.value == "") && (!firstKey) && (key === 8)) { // backspace
|
||||
clearInput()
|
||||
return
|
||||
}
|
||||
|
||||
if (key === 27) { // escape
|
||||
clearInput()
|
||||
return
|
||||
}
|
||||
|
||||
// Close and send on enter.
|
||||
if (key === 13) {
|
||||
if (textInput.value) {
|
||||
sendMessage()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (firstKey) firstKey = false
|
||||
}
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------
|
||||
|
||||
// Watch parent position and update side.
|
||||
watch(elementX, (elementX, _previous) => {
|
||||
const winW = window.innerWidth
|
||||
|
||||
// Logic depends on the side we are on.
|
||||
if (side.value === "right") {
|
||||
if (winW - elementX < 230) {
|
||||
switchSide()
|
||||
side.value = "left"
|
||||
}
|
||||
}
|
||||
|
||||
else {
|
||||
if (winW - elementX > 230) {
|
||||
switchSide()
|
||||
side.value = "right"
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
textInput = document.getElementById("textInput") as HTMLInputElement;
|
||||
window.addEventListener("keydown", onKeyDown);
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener("keydown", onKeyDown);
|
||||
});
|
||||
|
||||
return {
|
||||
typing
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
import { ref } from 'vue';
|
||||
import useScroll from "@/render/composables/useScroll";
|
||||
|
||||
const messagesRef = ref();
|
||||
|
||||
/* seed the canvas with messages */
|
||||
const seedCanvas = (messages: Message[]) => {
|
||||
messagesRef.value = messages;
|
||||
}
|
||||
|
||||
const addMessage = (message: Message) => {
|
||||
messagesRef.value.push(message);
|
||||
}
|
||||
|
||||
const updateMessage = (message: Message) => {
|
||||
|
||||
let target_message = messagesRef.value.filter((m: Message) => {
|
||||
return m.uid = message.uid;
|
||||
})[0];
|
||||
|
||||
if (target_message) {
|
||||
target_message = message;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default function useMessages() {
|
||||
|
||||
const { updateScrollRef, adjustScroll } = useScroll("messenger");
|
||||
|
||||
return {
|
||||
messagesRef,
|
||||
seedCanvas,
|
||||
addMessage,
|
||||
updateMessage
|
||||
};
|
||||
|
||||
}
|
||||
23
src/render/components/header.js
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
const Header =
|
||||
{
|
||||
setup() {
|
||||
const onNavBar = (command) => window.mainApi.send('nav-bar', command);
|
||||
return { onNavBar };
|
||||
},
|
||||
|
||||
template: `
|
||||
<button id="header-titlebar" />
|
||||
|
||||
<span id="header-menu">
|
||||
<button
|
||||
class="header-menu-button header-exit-button button"
|
||||
@click.prevent="onNavBar('close')"
|
||||
/>
|
||||
<button
|
||||
class="header-menu-button header-min-button button"
|
||||
@click.prevent="onNavBar('min')"
|
||||
/>
|
||||
</span> `
|
||||
}
|
||||
|
||||
export default Header;
|
||||
|
|
@ -1,89 +0,0 @@
|
|||
<template>
|
||||
<button id="titlebar" />
|
||||
<span id="menu">
|
||||
<button
|
||||
class="menuButton exitButton"
|
||||
@click.prevent="postNavBarExit"
|
||||
/>
|
||||
<button
|
||||
class="menuButton minimizeButton"
|
||||
@click.prevent="postNavBarMin"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
// import { postNavBarExit, postNavBarMin } from "@/render/ipc";
|
||||
import { defineComponent } from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "Header",
|
||||
setup() {
|
||||
const postNavBarExit = () => {};
|
||||
const postNavBarMin = () => {};
|
||||
return {
|
||||
postNavBarExit,
|
||||
postNavBarMin
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
#titlebar {
|
||||
position: fixed;
|
||||
|
||||
width: 100vw;
|
||||
height: 54px;
|
||||
|
||||
opacity: 0.75;
|
||||
|
||||
background-color: #EBEBEB;
|
||||
|
||||
-webkit-app-region: drag;
|
||||
|
||||
border: none;
|
||||
outline: none;
|
||||
|
||||
z-index: 1;
|
||||
|
||||
}
|
||||
|
||||
#menu {
|
||||
position: fixed;
|
||||
margin-left: 20px;
|
||||
margin-top: 20px;
|
||||
|
||||
z-index: 2;
|
||||
|
||||
}
|
||||
|
||||
.menuButton {
|
||||
border: none;
|
||||
outline:none;
|
||||
min-width: 14px;
|
||||
min-height: 14px;
|
||||
border-radius: 7px;
|
||||
}
|
||||
|
||||
.exitButton {
|
||||
background-color: #FF6157;
|
||||
}
|
||||
|
||||
.exitButton:active {
|
||||
background: #c14645;
|
||||
}
|
||||
|
||||
.minimizeButton {
|
||||
background-color: #FFC12F;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.minimizeButton:active {
|
||||
background-color: #c08e38;
|
||||
}
|
||||
</style>
|
||||
55
src/render/components/input.js
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import useText from "./control/text.js";
|
||||
import draggify from "./control/draggify.js";
|
||||
|
||||
const Input =
|
||||
{
|
||||
props: ["person"],
|
||||
|
||||
setup(props)
|
||||
{
|
||||
const initials = Vue.ref("");
|
||||
|
||||
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);
|
||||
|
||||
Vue.watch(() => props.person, (c, _p) => initials.value = getInitials(c));
|
||||
|
||||
return { initials, elementX, elementY, recording, leftSide, show };
|
||||
},
|
||||
|
||||
template: `
|
||||
<div id="input" :class="{ audio: recording }"
|
||||
:style="{ top: elementY + 'px', left: elementX + 'px' }"
|
||||
>
|
||||
|
||||
<div id="input-icon">{{ initials }}</div>
|
||||
|
||||
<input
|
||||
id="input-text" type="text"
|
||||
:class="[
|
||||
{ leftSide: leftSide },
|
||||
{ show: show },
|
||||
{ recording: recording }
|
||||
]"
|
||||
/>
|
||||
|
||||
</div> `
|
||||
}
|
||||
|
||||
function getInitials(name)
|
||||
{
|
||||
let rgx = new RegExp(/(\p{L}{1})\p{L}+/, 'gu');
|
||||
|
||||
let initials = [...name.matchAll(rgx)] || [];
|
||||
|
||||
return (
|
||||
(initials.shift()?.[1] || '') + (initials.pop()?.[1] || '')
|
||||
).toUpperCase();
|
||||
}
|
||||
|
||||
export default Input;
|
||||
|
|
@ -1,220 +0,0 @@
|
|||
<template>
|
||||
<div
|
||||
id="inputItem"
|
||||
class="input-item"
|
||||
:class="{ playing: recording }"
|
||||
:style="{ top: `${elementY}px`, left: `${elementX}px` }"
|
||||
>
|
||||
<div>{{ initials }}</div>
|
||||
|
||||
|
||||
<!-- Recording animation on space bar -->
|
||||
<span v-if="recording" class="play"></span>
|
||||
<span v-if="recording" class="pause"></span>
|
||||
|
||||
<!-- Show text input on key-down -->
|
||||
<input
|
||||
id="textInput"
|
||||
type="text"
|
||||
/>
|
||||
|
||||
<!-- Show suggestions menu on click -->
|
||||
<!-- <Menu /> -->
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
import { defineComponent } from "vue";
|
||||
import draggify from "@/render/composables/useDraggify";
|
||||
|
||||
|
||||
import useTextInputController from
|
||||
"@/render/components/controllers/inputItem.control.text";
|
||||
import useAudioInputController from
|
||||
"@/render/components/controllers/inputItem.control.audio";
|
||||
|
||||
export default defineComponent({
|
||||
name: "InputItem",
|
||||
|
||||
props: ["initials"],
|
||||
|
||||
setup() {
|
||||
|
||||
// Default values for position.
|
||||
const xStart = 15;
|
||||
const yStart = window.innerHeight - 200;
|
||||
|
||||
// Calculate position of inputItem on drag.
|
||||
const { elementX, elementY } = draggify("inputItem", xStart, yStart, 15);
|
||||
|
||||
// Controllers for text and audio.
|
||||
const { typing } = useTextInputController(elementX)
|
||||
const { recording } = useAudioInputController(typing)
|
||||
|
||||
return {
|
||||
elementX,
|
||||
elementY,
|
||||
recording
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
.input-item {
|
||||
position: absolute;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
padding: 0;
|
||||
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
|
||||
z-index: 3;
|
||||
|
||||
// To prevent window drag when overlapping with titlebar.
|
||||
-webkit-app-region: no-drag;
|
||||
|
||||
// border: 4px solid #C6C6C6;
|
||||
border-radius: 50%;
|
||||
|
||||
// Set opacity here to not affect child.
|
||||
background-color: rgba(235, 235, 235, 0.75);
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
box-shadow: 0px 0px 10px 0px rgba(0,0,0,0.15);
|
||||
|
||||
.play,
|
||||
.pause {
|
||||
z-index: 5;
|
||||
&::before,
|
||||
&::after {
|
||||
-webkit-border-radius: 1000px;
|
||||
-moz-border-radius: 1000px;
|
||||
border-radius: 1000px;
|
||||
content: "";
|
||||
position: absolute;
|
||||
height: 2.35em;
|
||||
width: 2.35em;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
top: 50%;
|
||||
z-index: 0;
|
||||
}
|
||||
}
|
||||
.play::before {
|
||||
box-shadow: 0 0 0 rgba(195, 195, 195, 0);
|
||||
}
|
||||
.pause {
|
||||
opacity: 0;
|
||||
}
|
||||
&.playing {
|
||||
.play {
|
||||
opacity: 0;
|
||||
}
|
||||
.pause {
|
||||
opacity: 1;
|
||||
&::before {
|
||||
-moz-animation: circle1 1.5s infinite ease-in-out;
|
||||
-o-animation: circle1 1.5s infinite ease-in-out;
|
||||
-webkit-animation: circle1 1.5s infinite ease-in-out;
|
||||
animation: circle1 1.5s infinite ease-in-out;
|
||||
}
|
||||
&::after {
|
||||
-moz-animation: circle2 2.2s infinite ease-in-out;
|
||||
-o-animation: circle2 2.2s infinite ease-in-out;
|
||||
-webkit-animation: circle2 2.2s infinite ease-in-out;
|
||||
animation: circle2 2.2s infinite ease-in-out;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.rec-icon {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background-color: red;
|
||||
transform: translate(14px);
|
||||
|
||||
animation-name: rec;
|
||||
animation-duration: 0.5s;
|
||||
}
|
||||
|
||||
@keyframes rec {
|
||||
from {
|
||||
transform: translate(14px) scale(0.3);
|
||||
}
|
||||
to {
|
||||
transform: translate(14px) scale(1.0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes circle1 {
|
||||
0%,
|
||||
100% {
|
||||
box-shadow: 0 0 0 0.4em rgba(195, 195, 195, 0.4);
|
||||
}
|
||||
25% {
|
||||
box-shadow: 0 0 0 0.15em rgba(195, 195, 195, 0.15);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 0 0.55em rgba(195, 195, 195, 0.55);
|
||||
}
|
||||
75% {
|
||||
box-shadow: 0 0 0 0.25em rgba(195, 195, 195, 0.25);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes circle2 {
|
||||
0%,
|
||||
100% {
|
||||
box-shadow: 0 0 0 0.25em rgba(195, 195, 195, 0.15);
|
||||
}
|
||||
25% {
|
||||
box-shadow: 0 0 0 0.4em rgba(195, 195, 195, 0.3);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 0 0.15em rgba(195, 195, 195, 0.05);
|
||||
}
|
||||
75% {
|
||||
box-shadow: 0 0 0 0.55em rgba(195, 195, 195, 0.45);
|
||||
}
|
||||
}
|
||||
|
||||
#textInput {
|
||||
position: absolute;
|
||||
|
||||
opacity: 0;
|
||||
|
||||
min-width: 150px;
|
||||
height: 16px;
|
||||
|
||||
border-radius: 18px;
|
||||
|
||||
padding: 10px;
|
||||
margin-right: 10px;
|
||||
margin-left: 10px;
|
||||
|
||||
outline: none;
|
||||
border: none;
|
||||
pointer-events: none;
|
||||
|
||||
background-color: white;
|
||||
|
||||
z-index: -1;
|
||||
|
||||
box-shadow: 0px 0px 10px 0px rgba(0,0,0,0.15);
|
||||
|
||||
transform: translateX(50px) scale(0.3);
|
||||
|
||||
}
|
||||
|
||||
</style>
|
||||
58
src/render/components/login.js
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
const Login =
|
||||
{
|
||||
setup()
|
||||
{
|
||||
const usr = Vue.ref("");
|
||||
const pwd = Vue.ref("");
|
||||
|
||||
const submitForm = async () => {
|
||||
const success = await window.mainApi.invoke("login", usr.value, pwd.value);
|
||||
if (!success) shake("login-form");
|
||||
}
|
||||
|
||||
return { usr, pwd, submitForm };
|
||||
},
|
||||
|
||||
template: `
|
||||
<form id="login" @submit.prevent="submitForm">
|
||||
|
||||
<!-- username and password forms -->
|
||||
<div id="login-form">
|
||||
<!-- username -->
|
||||
<div class="login-input-box">
|
||||
<input class="login-input" type="text" v-model="usr" />
|
||||
<div class="login-input-modifier">Email</div>
|
||||
</div>
|
||||
|
||||
<!-- password -->
|
||||
<div class="login-input-box">
|
||||
<input class="login-input" type="password" v-model="pwd" />
|
||||
<div class="login-input-modifier">Password</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- submit button; position: fixed -->
|
||||
<button
|
||||
class="login-submit-button button"
|
||||
type="submit"
|
||||
>
|
||||
Login
|
||||
</button>
|
||||
|
||||
</form> `
|
||||
}
|
||||
|
||||
/** Shake a div to indicate incorrect credentials */
|
||||
const shake = (elementId) =>
|
||||
{
|
||||
const el = document.getElementById(elementId);
|
||||
|
||||
if (el)
|
||||
el.classList.remove("shake");
|
||||
|
||||
setTimeout(() => {
|
||||
if (el) el.classList.add("shake");
|
||||
}, 250);
|
||||
}
|
||||
|
||||
export default Login;
|
||||
|
|
@ -1,180 +0,0 @@
|
|||
<template>
|
||||
<form id="login" @submit.prevent="submitForm">
|
||||
|
||||
|
||||
<!-- login title -->
|
||||
<div id="loginTitle">
|
||||
<div>Login</div>
|
||||
</div>
|
||||
|
||||
<!-- username and password forms -->
|
||||
<div id="loginBox">
|
||||
<!-- 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="submitButton button" type="submit">Submit</button>
|
||||
|
||||
</form>
|
||||
|
||||
<!-- back to login button: position: fixed -->
|
||||
<!-- <button class="createAccountButton button" @click.prevent="switchView">Create account</button> -->
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
import { defineComponent, ref } from "vue";
|
||||
import { setProfile } from '@/render/composables/useProfile';
|
||||
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 {
|
||||
|
||||
const profile = await invokeLogin({
|
||||
email: usr.value,
|
||||
password: pwd.value
|
||||
}) as Profile;
|
||||
|
||||
setProfile(profile)
|
||||
|
||||
|
||||
} catch(e) {
|
||||
console.log(e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
#loginTitle {
|
||||
font-family: "SF Pro Text";
|
||||
min-width: 181px;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
#loginBox {
|
||||
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;
|
||||
}
|
||||
|
||||
.submitButton {
|
||||
font-family: "SF Compact Display";
|
||||
position: fixed;
|
||||
margin-top: 141px;
|
||||
background-color: #58C4FD;
|
||||
color: white;
|
||||
padding: 10px 30px;
|
||||
border-radius: 20px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.submitButton:active {
|
||||
background-color: #4296C3;
|
||||
}
|
||||
|
||||
.createAccountButton {
|
||||
position: absolute;
|
||||
border: none;
|
||||
background-color: #EBEBEB;
|
||||
text-decoration: none;
|
||||
color: #58C4FD;
|
||||
font-weight: bold;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
}
|
||||
|
||||
.button {
|
||||
border: none;
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.invalid {
|
||||
margin-bottom: 30px;
|
||||
font-family: "SF Compact Display";
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
color: #F53737;
|
||||
}
|
||||
|
||||
</style>
|
||||
60
src/render/components/message.js
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import Bubble from "./bubble.js";
|
||||
import Context from "./context.js";
|
||||
|
||||
const Message =
|
||||
{
|
||||
components: {
|
||||
Bubble,
|
||||
Context
|
||||
},
|
||||
|
||||
props: ["modifier", "content", "context", "avatar", "id"],
|
||||
|
||||
setup(props)
|
||||
{
|
||||
return { calcChild };
|
||||
},
|
||||
|
||||
template: `
|
||||
<div :id="id" :class="'message ' + modifier + '-message'">
|
||||
|
||||
<Bubble
|
||||
v-for="(c, index) in content"
|
||||
:category="c.category"
|
||||
:text="c.text"
|
||||
:blob="c.blob"
|
||||
:modifier="modifier"
|
||||
:child="calcChild(index, content.length)"
|
||||
/>
|
||||
|
||||
<Context
|
||||
:modifier="modifier"
|
||||
:context="context"
|
||||
:avatar="avatar"
|
||||
: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;
|
||||
102
src/render/components/messenger.js
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
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 person = Vue.ref("");
|
||||
const messages = Vue.ref([{
|
||||
modifier: "ai",
|
||||
content: [{
|
||||
category: "text/html",
|
||||
text: `<div class="mail">
|
||||
<h4>About HTML Parser</h4>
|
||||
<p>Return the text of the most recently opened start tag. This should not normally be needed for structured processing, but may be useful in dealing with HTML “as deployed” or for re-generating input with minimal changes (whitespace between attributes can be preserved, etc.).</p>
|
||||
</div>`,
|
||||
blob: null
|
||||
}],
|
||||
context: "Email from David Spade",
|
||||
avatar: null,
|
||||
id: 0
|
||||
}]);
|
||||
|
||||
const onDrop = async (e) => {
|
||||
const file = e.dataTransfer.items[0].getAsFile();
|
||||
|
||||
const buffer = await file.arrayBuffer();
|
||||
const b64String = await window.mainApi.invoke("encode", buffer);
|
||||
|
||||
window.mainApi.send("message", {
|
||||
category: file.type,
|
||||
text: file.name,
|
||||
blob: b64String
|
||||
});
|
||||
}
|
||||
|
||||
Vue.onMounted(() => {
|
||||
|
||||
window.mainApi.on("message", (message) => {
|
||||
if (message.person)
|
||||
{
|
||||
person.value = message.person;
|
||||
messages.value = message.messages;
|
||||
}
|
||||
else if (message.name)
|
||||
{
|
||||
person.value = message;
|
||||
}
|
||||
else if (message.content)
|
||||
{
|
||||
messages.value.push(message);
|
||||
}
|
||||
else
|
||||
{
|
||||
messages.value[messages.value.length - 1].content.push(message);
|
||||
}
|
||||
});
|
||||
|
||||
initScroll("messenger");
|
||||
window.mainApi.send("messenger");
|
||||
});
|
||||
|
||||
Vue.onUnmounted(() => {
|
||||
messages.value = [];
|
||||
});
|
||||
|
||||
return { person, messages, onDrop };
|
||||
},
|
||||
|
||||
template: `
|
||||
<Settings />
|
||||
<WS />
|
||||
<Input :person="person" />
|
||||
|
||||
<div id="messenger"
|
||||
@drop="onDrop($event)"
|
||||
@dragover.prevent
|
||||
@dragenter.prevent
|
||||
>
|
||||
<Message
|
||||
v-for="message in messages"
|
||||
:modifier="message.modifier"
|
||||
:content="message.content"
|
||||
:context="message.context"
|
||||
:avatar="message.avatar"
|
||||
:id="message.id"
|
||||
/>
|
||||
</div> `
|
||||
}
|
||||
|
||||
export default Messenger;
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
<template>
|
||||
|
||||
<!-- Position fixed items -->
|
||||
<InputItem :initials="profile.initials"/>
|
||||
<Settings />
|
||||
<div id="recIcon" />
|
||||
|
||||
<!-- List of message bubbles. -->
|
||||
<div id="messenger">
|
||||
<Bubble
|
||||
v-for="message in messages"
|
||||
:text="message.content.text"
|
||||
:context="message.context"
|
||||
:key="message[0]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
import { defineComponent, onMounted, onUnmounted } from "vue";
|
||||
import InputItem from "@/render/components/inputItem.vue";
|
||||
import Settings from "@/render/components/settings.vue";
|
||||
import Bubble from "@/render/components/bubble.vue";
|
||||
|
||||
import { profile } from "@/render/composables/useProfile";
|
||||
import { messages } from "@/render/composables/useMessages";
|
||||
|
||||
export default defineComponent({
|
||||
name: "Messenger",
|
||||
|
||||
components: {
|
||||
InputItem,
|
||||
Settings,
|
||||
Bubble
|
||||
},
|
||||
|
||||
setup() {
|
||||
return {
|
||||
messages,
|
||||
profile
|
||||
};
|
||||
|
||||
},
|
||||
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
#messenger {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#messenger::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#recIcon {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
|
||||
opacity: 0;
|
||||
transform: scale(0.0);
|
||||
|
||||
margin-top: 24px;
|
||||
margin-right: 66px;
|
||||
|
||||
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background-color: #FF3B3B;
|
||||
|
||||
z-index: 2;
|
||||
|
||||
}
|
||||
|
||||
</style>
|
||||
28
src/render/components/settings.js
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
|
||||
const Settings =
|
||||
{
|
||||
setup() {
|
||||
const active = Vue.ref(false);
|
||||
|
||||
window.addEventListener("keydown", (e) => {
|
||||
console.log("toggle settings");
|
||||
})
|
||||
|
||||
const logout = async () => {
|
||||
await window.mainApi.send("logout");
|
||||
}
|
||||
|
||||
return { active, logout };
|
||||
},
|
||||
|
||||
template: `
|
||||
<div v-if="active" id="settings" >
|
||||
|
||||
<button class="settings-logout-button button" @click="logout">
|
||||
Logout
|
||||
</button>
|
||||
|
||||
</div>`
|
||||
}
|
||||
|
||||
export default Settings;
|
||||
|
|
@ -1,164 +0,0 @@
|
|||
<template>
|
||||
|
||||
<!-- Settings icon. -->
|
||||
<button
|
||||
class="settingsIcon"
|
||||
v-if="toggleSettings == false"
|
||||
@click="onActive"
|
||||
>
|
||||
<div class="settingsButtonDot"></div>
|
||||
<div class="settingsButtonDot"></div>
|
||||
<div class="settingsButtonDot"></div>
|
||||
</button>
|
||||
|
||||
<!-- Settings div. -->
|
||||
<div id="settings" v-show="toggleSettings">
|
||||
</div>
|
||||
|
||||
<button
|
||||
v-if="toggleSettings"
|
||||
class="settingsOption"
|
||||
@click="onLogout"
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
import { defineComponent, ref } from "vue";
|
||||
import { clearProfile } from "@/render/composables/useProfile"
|
||||
import { invokeLogout } from "@/render/ipc";
|
||||
|
||||
export default defineComponent({
|
||||
name: "Settings",
|
||||
|
||||
setup() {
|
||||
const toggleSettings = ref(false);
|
||||
|
||||
// Listen for escape key to close settings.
|
||||
const onEscape = (e: any) => {
|
||||
if(e.key === "Escape") {
|
||||
toggleSettings.value = false;
|
||||
}
|
||||
window.removeEventListener("keydown", onEscape);
|
||||
}
|
||||
|
||||
// When settings is showing.
|
||||
const onActive = () => {
|
||||
toggleSettings.value = true;
|
||||
window.addEventListener("keydown", onEscape);
|
||||
}
|
||||
|
||||
// We ask server to log us out.
|
||||
const onLogout = async () => {
|
||||
|
||||
console.log("Submitting logout request.");
|
||||
|
||||
try {
|
||||
await invokeLogout();
|
||||
clearProfile();
|
||||
} catch(e) {
|
||||
console.log('error')
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return {
|
||||
onActive,
|
||||
toggleSettings,
|
||||
onLogout
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
#settings {
|
||||
position: fixed;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
|
||||
background-color: #EBEBEB;
|
||||
|
||||
opacity: 0.75;
|
||||
|
||||
z-index: 4;
|
||||
|
||||
animation-name: appear;
|
||||
animation-duration: 0.5s;
|
||||
}
|
||||
|
||||
@keyframes appear {
|
||||
from {
|
||||
opacity: 0
|
||||
}
|
||||
to {
|
||||
opacity: 0.75
|
||||
}
|
||||
}
|
||||
|
||||
.settingsIcon {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
|
||||
border: none;
|
||||
outline: none;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
padding: 5px;
|
||||
margin-right: 20px;
|
||||
margin-top: 19px;
|
||||
|
||||
z-index: 2;
|
||||
background-color: Transparent;
|
||||
}
|
||||
|
||||
.settingsIcon:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.settingsButtonDot {
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
|
||||
margin: 2px;
|
||||
border-radius: 2.5px;
|
||||
background-color: #9B9B9B;
|
||||
|
||||
}
|
||||
|
||||
.settingsOption {
|
||||
font-family: "SF Compact Display";
|
||||
background-color: #B7B7B7;
|
||||
padding: 10px 30px;
|
||||
border-radius: 20px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
border: none;
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.settingsOption:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
50
src/render/components/splash.js
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
const Splash =
|
||||
{
|
||||
template: `
|
||||
<div id="splash">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="57.046"
|
||||
height="52.759"
|
||||
viewBox="0 0 57.046 52.759"
|
||||
>
|
||||
<g transform="translate(-4127 -507)">
|
||||
<g transform="translate(3949 332.206)">
|
||||
<g transform="translate(178 174.794)">
|
||||
<g transform="translate(0 27.405)">
|
||||
<circle
|
||||
cx="12.677"
|
||||
cy="12.677"
|
||||
r="12.677"
|
||||
transform="translate(0 0)"
|
||||
fill="#383838"
|
||||
/>
|
||||
<circle
|
||||
cx="12.677"
|
||||
cy="12.677"
|
||||
r="12.677"
|
||||
transform="translate(31.692 0)"
|
||||
fill="#383838"
|
||||
/>
|
||||
</g>
|
||||
<circle
|
||||
cx="12.677"
|
||||
cy="12.677"
|
||||
r="12.677"
|
||||
transform="translate(15.822 0)"
|
||||
fill="#383838"
|
||||
/>
|
||||
<path
|
||||
d="M36.27,83.67"
|
||||
transform="translate(-23.569 -43.588)"
|
||||
fill="#ff0"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
export default Splash;
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
<template>
|
||||
<div id="splash">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="57.046"
|
||||
height="52.759"
|
||||
viewBox="0 0 57.046 52.759"
|
||||
>
|
||||
<g transform="translate(-4127 -507)">
|
||||
<g transform="translate(3949 332.206)">
|
||||
<g transform="translate(178 174.794)">
|
||||
<g transform="translate(0 27.405)">
|
||||
<circle
|
||||
cx="12.677"
|
||||
cy="12.677"
|
||||
r="12.677"
|
||||
transform="translate(0 0)"
|
||||
fill="#383838"
|
||||
/>
|
||||
<circle
|
||||
cx="12.677"
|
||||
cy="12.677"
|
||||
r="12.677"
|
||||
transform="translate(31.692 0)"
|
||||
fill="#383838"
|
||||
/>
|
||||
</g>
|
||||
<circle
|
||||
cx="12.677"
|
||||
cy="12.677"
|
||||
r="12.677"
|
||||
transform="translate(15.822 0)"
|
||||
fill="#383838"
|
||||
/>
|
||||
<path
|
||||
d="M36.27,83.67"
|
||||
transform="translate(-23.569 -43.588)"
|
||||
fill="#ff0"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
import { defineComponent } from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "Splash",
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
#splash {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
}
|
||||
|
||||
</style>
|
||||
35
src/render/components/ws.js
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
|
||||
const WS =
|
||||
{
|
||||
setup()
|
||||
{
|
||||
const status = Vue.ref();
|
||||
|
||||
Vue.onMounted(() => {
|
||||
|
||||
window.mainApi.on("ws-status", (val) => {
|
||||
status.value = val;
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
return { status }
|
||||
},
|
||||
|
||||
template: `
|
||||
<div id="ws">
|
||||
|
||||
<img id="ws-logo" src="./assets/connectLogo.svg"
|
||||
:class="{ move: status }"
|
||||
>
|
||||
|
||||
<div id="ws-dots"
|
||||
:class="{ move: status }"
|
||||
>
|
||||
<div class="ws-dot"></div>
|
||||
</div>
|
||||
|
||||
</div> `
|
||||
}
|
||||
|
||||
export default WS;
|
||||
|
|
@ -1,199 +0,0 @@
|
|||
import { onMounted, ref, onUnmounted } from "vue";
|
||||
|
||||
//---Dragabble Helper Funcs--------------------------------------
|
||||
|
||||
// Calculate distance to nearest side.
|
||||
function calcSideProximity (elementX: number, winW: number) {
|
||||
|
||||
// Calc right short.
|
||||
let short = winW - elementX - 40;
|
||||
|
||||
// See if it's left short.
|
||||
if (elementX + 20 < winW / 2) {
|
||||
short = elementX
|
||||
}
|
||||
|
||||
return short
|
||||
}
|
||||
|
||||
// Update elementX or elementY value on window resize
|
||||
function calcPosition (elementPosition: number, percent: number,
|
||||
short: number, win: number) {
|
||||
|
||||
// Is it close to the right/bottom side?
|
||||
if (percent > 0.75) {
|
||||
elementPosition = win - short - 40;
|
||||
}
|
||||
|
||||
// Is it not close to a side?
|
||||
if (percent < 0.75 && percent > 0.25) {
|
||||
elementPosition = win * percent
|
||||
}
|
||||
|
||||
return elementPosition
|
||||
}
|
||||
|
||||
export default function draggify(elementId: string, xStart: number,
|
||||
yStart: number, margin: number) {
|
||||
|
||||
let element: HTMLElement | null;
|
||||
|
||||
// Cords of inputItem.
|
||||
const elementX = ref(0);
|
||||
const elementY = ref(0);
|
||||
|
||||
// Position of inputItem on terms of percentage of window.
|
||||
let percentX: number;
|
||||
let percentY: number;
|
||||
|
||||
// How close inputItem is to closest X or Y side.
|
||||
let xShort: number;
|
||||
let yShort: number;
|
||||
|
||||
// Keep track of window size;
|
||||
let winW = window.innerWidth;
|
||||
let winH = window.innerHeight;
|
||||
|
||||
//---Reposition Anime-----------------------------------------------
|
||||
|
||||
// Move element to target smoothly.
|
||||
const repositionAnime = (xChange: number, yChange: number) => {
|
||||
const xStep = xChange / 6000;
|
||||
const yStep = yChange / 6000;
|
||||
|
||||
for (let i = 1; i <= 6000; i++) {
|
||||
|
||||
setTimeout(() => {
|
||||
elementX.value += xStep;
|
||||
elementY.value += yStep;
|
||||
}, 16) // 60fps
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
//---Event Handlers-----------------------------------------------
|
||||
|
||||
// Update the position of inputItem on mouse dragging.
|
||||
const onMouseMove = (e: any) => {
|
||||
e.preventDefault()
|
||||
element = document.getElementById(elementId);
|
||||
|
||||
if (element) {
|
||||
const inputItemRect = element.getBoundingClientRect();
|
||||
elementX.value = inputItemRect.left + e.movementX;
|
||||
elementY.value = inputItemRect.top + e.movementY;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Add an event listener for dragging.
|
||||
const onMouseDown = (_e: any) => {
|
||||
_e.preventDefault()
|
||||
window.addEventListener('mousemove', onMouseMove, true);
|
||||
}
|
||||
|
||||
// Update position references when user is done moving targetEl.
|
||||
const onMouseUp = (_e: any) => {
|
||||
window.removeEventListener('mousemove', onMouseMove, true);
|
||||
|
||||
// See if and calculate reposition.
|
||||
let x = 0; // vector change
|
||||
let y = 0;
|
||||
|
||||
if (elementX.value < 0) {
|
||||
x = (elementX.value - margin)*-1
|
||||
}
|
||||
|
||||
if (elementX.value > winW - 40) {
|
||||
const b = winW - 40 - margin;
|
||||
x = (elementX.value - b)*-1
|
||||
}
|
||||
|
||||
// Reposition y
|
||||
if (elementY.value < 0) {
|
||||
y = (elementY.value - margin)*-1
|
||||
}
|
||||
|
||||
if (elementY.value > winH - 40) {
|
||||
const d = winH - 40 - margin;
|
||||
y = (elementY.value - d)*-1
|
||||
}
|
||||
|
||||
// Reposition if needed.
|
||||
if (x !== 0 || y !== 0) repositionAnime(x, y);
|
||||
|
||||
xShort = calcSideProximity(elementX.value, winW);
|
||||
yShort = calcSideProximity(elementY.value, winH);
|
||||
|
||||
// Update percentages.
|
||||
percentX = elementX.value / winW;
|
||||
percentY = elementY.value / winH;
|
||||
|
||||
// Save position.
|
||||
const position = {
|
||||
x: elementX.value,
|
||||
y: elementY.value
|
||||
}
|
||||
window.localStorage.setItem("inputItem_position", JSON.stringify(position));
|
||||
}
|
||||
|
||||
// Update position of targetEl on windowResize.
|
||||
const onWindowResize = (_e: any) => {
|
||||
|
||||
// Update window dimensions.
|
||||
winW = window.innerWidth;
|
||||
winH = window.innerHeight;
|
||||
|
||||
elementX.value = calcPosition(elementX.value, percentX, xShort, winW);
|
||||
elementY.value = calcPosition(elementY.value, percentY, yShort, winH);
|
||||
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------
|
||||
|
||||
onMounted(() => {
|
||||
element = document.getElementById(elementId);
|
||||
|
||||
if (element) {
|
||||
element.addEventListener('mousedown', onMouseDown, false);
|
||||
}
|
||||
|
||||
window.addEventListener("mouseup", onMouseUp, false);
|
||||
|
||||
// Initialize the positional references.
|
||||
percentX = elementX.value / window.innerWidth;
|
||||
percentY = elementY.value / window.innerHeight;
|
||||
|
||||
xShort = calcSideProximity(elementX.value, window.innerWidth);
|
||||
yShort = calcSideProximity(elementX.value, window.innerHeight);
|
||||
|
||||
// Then, we can listen for window resize.
|
||||
window.addEventListener('resize', onWindowResize, false);
|
||||
});
|
||||
|
||||
// Try loading initPosition, otherwise set default values
|
||||
let initPosition: any;
|
||||
const rawData = window.localStorage.getItem("inputItem_position")
|
||||
|
||||
if (rawData) {
|
||||
initPosition = JSON.parse(rawData)
|
||||
} else {
|
||||
initPosition = {x: xStart, y: yStart}
|
||||
}
|
||||
|
||||
elementX.value = initPosition.x;
|
||||
elementY.value = initPosition.y;
|
||||
|
||||
// remove event listeners on component dismount.
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', onWindowResize);
|
||||
window.removeEventListener('mouseup', onMouseUp);
|
||||
if (element) element.removeEventListener('mousedown', onMouseDown);
|
||||
})
|
||||
|
||||
return {
|
||||
elementX,
|
||||
elementY
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
|
||||
import { IpcRendererEvent } from "electron";
|
||||
|
||||
export class IpcRendererListener<InputParam> implements IIpcListener<InputParam> {
|
||||
|
||||
readonly channel: string;
|
||||
|
||||
readonly _listenerCallback: IpcListenerCallback<InputParam>;
|
||||
|
||||
constructor(options: {
|
||||
channel: string;
|
||||
listenerCallback: IpcListenerCallback<InputParam>;
|
||||
}) {
|
||||
this.channel = options.channel;
|
||||
this._listenerCallback = options.listenerCallback;
|
||||
}
|
||||
|
||||
listen() {
|
||||
this.remove();
|
||||
window.ipcRenderer.on(this.channel, this._onPost);
|
||||
}
|
||||
|
||||
remove() {
|
||||
window.ipcRenderer.removeAllListeners(this.channel);
|
||||
}
|
||||
|
||||
private _onPost = (_e: IpcRendererEvent, payload: InputParam): void => {
|
||||
console.log(`[IPC] Post: ${this.channel}`);
|
||||
this._listenerCallback(payload);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
export default function useIpcRenderer () {
|
||||
|
||||
const invoke = async (endpoint: string, payload: any) => {
|
||||
try {
|
||||
const res = await window.ipcRenderer.invoke(endpoint, payload);
|
||||
return res;
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
const post = (endpoint: string, payload: any) => {
|
||||
window.ipcRenderer.send(endpoint, payload);
|
||||
};
|
||||
|
||||
return {
|
||||
invoke,
|
||||
post,
|
||||
}
|
||||
}
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
// shared
|
||||
import { ref, Ref } from "vue";
|
||||
import useScroll from "@/render/composables/useScroll";
|
||||
|
||||
export const messages: Ref<Array<Message>> = ref([]);
|
||||
|
||||
export const setMessages: IpcListenerCallback<Array<Message>> = (payload) => {
|
||||
messages.value = payload as Array<Message>;
|
||||
}
|
||||
|
||||
export const addMessage: IpcListenerCallback<Message> = (payload) => {
|
||||
messages.value.push(payload as Message);
|
||||
}
|
||||
|
||||
export const updateMessage: IpcListenerCallback<Message> = (payload) => {
|
||||
const message = payload as Message;
|
||||
|
||||
let targetMessage = messages.value.filter((m: Message) => {
|
||||
return m.uid = message.uid;
|
||||
})[0];
|
||||
|
||||
if (targetMessage) {
|
||||
targetMessage = message;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default {
|
||||
messages,
|
||||
setMessages,
|
||||
addMessage,
|
||||
updateMessage
|
||||
};
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
// shared
|
||||
import { ref } from "vue";
|
||||
|
||||
export const profile = ref();
|
||||
|
||||
export const authComplete = ref(false);
|
||||
|
||||
export const setProfile: IpcListenerCallback<Profile | null> = (payload) => {
|
||||
payload ? profile.value = payload : clearProfile();
|
||||
showRender();
|
||||
};
|
||||
|
||||
export const clearProfile = () => {
|
||||
profile.value = null;
|
||||
};
|
||||
|
||||
export const showRender = () => {
|
||||
authComplete.value = true;
|
||||
};
|
||||
|
||||
export default {
|
||||
setProfile,
|
||||
clearProfile,
|
||||
profile,
|
||||
showRender,
|
||||
authComplete,
|
||||
};
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
|
||||
|
||||
export default function useScroll(element: string) {
|
||||
|
||||
let isScrolledToBottom: boolean;
|
||||
const view = document.getElementById(element)
|
||||
|
||||
// Update isScrolledToBottom
|
||||
const updateScrollRef = () => {
|
||||
if (view) isScrolledToBottom = view.scrollHeight - view.clientHeight <= view.scrollTop + 1;
|
||||
return isScrolledToBottom;
|
||||
}
|
||||
|
||||
// Adjust scroll after we add content to the messenger.
|
||||
const adjustScroll = () => {
|
||||
if (view) {
|
||||
view.scrollTo({
|
||||
top: view.scrollHeight - view.clientHeight,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
updateScrollRef,
|
||||
adjustScroll,
|
||||
};
|
||||
|
||||
}
|
||||
17
src/render/index.html
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="stylesheet" href="./main.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="app"></div>
|
||||
|
||||
<!-- vue@3.2.11 -->
|
||||
<script src="./vendor/vue.js"></script>
|
||||
|
||||
<script type="module" src="./index.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
2
src/render/index.js
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
import App from "./components/app.js";
|
||||
Vue.createApp(App).mount("#app");
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
|
||||
import useIpc from "@/render/composables/useIpcRend";
|
||||
import * as rendererListeners from "./listeners";
|
||||
|
||||
const { post, invoke } = useIpc();
|
||||
|
||||
/**
|
||||
*
|
||||
* Account and auth related endpoints
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
export const invokeLogin = async (
|
||||
payload: LoginPayload
|
||||
): Promise<Profile | Error> => (
|
||||
await invoke('invoke-account-login', JSON.stringify(payload))
|
||||
);
|
||||
|
||||
export const invokeLogout = async (): Promise<void> => (
|
||||
await invoke("invoke-account-logout", null)
|
||||
);
|
||||
|
||||
/**
|
||||
*
|
||||
* Audio endpoints
|
||||
*
|
||||
*/
|
||||
|
||||
export const postAudioChunk = (chunk: ArrayBuffer): void => (
|
||||
post("post-audio-collect", chunk)
|
||||
);
|
||||
|
||||
export const invokeReturnAudio = async (): Promise<ArrayBuffer[] | Error> => (
|
||||
await invoke("invoke-audio-flush", null)
|
||||
);
|
||||
|
||||
/**
|
||||
*
|
||||
* Crimata Platform (session) endpoints
|
||||
*
|
||||
*/
|
||||
|
||||
export const invokeSession = async (cid: string): Promise<Profile | Error> => (
|
||||
await invoke("messenger-init", cid)
|
||||
);
|
||||
|
||||
export const postMessage = (payload: Message): void => (
|
||||
post('post-session-send', payload)
|
||||
);
|
||||
|
||||
export const postAppMount = (): void => (
|
||||
post('post-app-mount', null)
|
||||
);
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* Ipc Renderer Listeners
|
||||
*
|
||||
*/
|
||||
|
||||
let ipcListeners: IPCListeners = {};
|
||||
|
||||
export const initIpcRendererListeners = () => {
|
||||
for (const [key, listener] of Object.entries(rendererListeners)) {
|
||||
if (!(key in ipcListeners)) {
|
||||
ipcListeners[key] = listener;
|
||||
listener.listen();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const removeListeners = () => {
|
||||
for (const [key, listener] of Object.entries(rendererListeners)) {
|
||||
listener.remove();
|
||||
}
|
||||
ipcListeners = {};
|
||||
};
|
||||
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
|
||||
import { IpcRendererListener } from "./composables/useIpcRend"
|
||||
import { setProfile } from "./composables/useProfile";
|
||||
import { setMessages, addMessage, updateMessage } from "./composables/useMessages";
|
||||
|
||||
const SET_PROFILE_CHANNEL = "set-profile";
|
||||
|
||||
const INIT_MESSAGES_CHANNEL = "init-messages";
|
||||
const ADD_MESSAGE_CHANNEL = "add-message";
|
||||
const UPDATE_MESSAGE_CHANNEL = "update-message";
|
||||
|
||||
export const setProfileListener = new IpcRendererListener({
|
||||
channel: SET_PROFILE_CHANNEL,
|
||||
listenerCallback: setProfile
|
||||
});
|
||||
|
||||
export const initMessagesListener = new IpcRendererListener({
|
||||
channel: INIT_MESSAGES_CHANNEL,
|
||||
listenerCallback: setMessages
|
||||
});
|
||||
|
||||
|
||||
export const addMessagesListener = new IpcRendererListener({
|
||||
channel: ADD_MESSAGE_CHANNEL,
|
||||
listenerCallback: addMessage
|
||||
});
|
||||
|
||||
export const updateMessagesListener = new IpcRendererListener({
|
||||
channel: UPDATE_MESSAGE_CHANNEL,
|
||||
listenerCallback: updateMessage
|
||||
});
|
||||
|
||||
571
src/render/main.css
Normal file
|
|
@ -0,0 +1,571 @@
|
|||
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;
|
||||
}
|
||||
|
||||
.contacts {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border-radius: inherit;
|
||||
background-color: #DBDBDB;
|
||||
}
|
||||
|
||||
.contacts > li {
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.contacts img {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.contacts .info {
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.contacts .name {
|
||||
|
||||
}
|
||||
|
||||
.contacts .email {
|
||||
font-family: SF Compact Display;
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
color: #898989;
|
||||
}
|
||||
|
||||
.mail {
|
||||
padding: 12px;
|
||||
margin: 0px;
|
||||
background-color: #D6F2FF;
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
.mail h4 {
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
.mail p {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
#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;
|
||||
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 > p {
|
||||
margin: 0px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.bubble > div {
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
.bubble > img {
|
||||
border-radius: inherit;
|
||||
display: block;
|
||||
max-width: inherit;
|
||||
}
|
||||
|
||||
.bubble a {
|
||||
border-radius: inherit;
|
||||
font-family: "SF Compact Rounded";
|
||||
background-color: #D9D9D9;
|
||||
font-weight: bold;
|
||||
padding: 12px;
|
||||
color: #727272;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.context-avatar img {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
/* ---------- shared between components ---------- */
|
||||
|
||||
.button {
|
||||
border: none;
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/*.button:hover {
|
||||
cursor: pointer;
|
||||
}*/
|
||||
|
||||
.audio {
|
||||
animation-name: audio-anim;
|
||||
animation-duration: 2s;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
@keyframes audio-anim {
|
||||
0%,
|
||||
100% {
|
||||
box-shadow: 0 0 0 0.4em rgba(195, 195, 195, 0.4), 0 0 0 0.25em rgba(195, 195, 195, 0.15);
|
||||
}
|
||||
25% {
|
||||
box-shadow: 0 0 0 0.15em rgba(195, 195, 195, 0.15), 0 0 0 0.4em rgba(195, 195, 195, 0.3);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 0 0.55em rgba(195, 195, 195, 0.55), 0 0 0 0.15em rgba(195, 195, 195, 0.05);
|
||||
}
|
||||
75% {
|
||||
box-shadow: 0 0 0 0.25em rgba(195, 195, 195, 0.25), 0 0 0 0.55em rgba(195, 195, 195, 0.45);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
|
||||
// src/main.ts
|
||||
|
||||
import App from "./App.vue";
|
||||
|
||||
import mitt from "mitt";
|
||||
import { createApp } from "vue";
|
||||
|
||||
import { initIpcRendererListeners } from "./ipc"
|
||||
|
||||
|
||||
// Handle ipcMain events.
|
||||
initIpcRendererListeners();
|
||||
|
||||
// Handle events.
|
||||
const emitter = mitt();
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
app.provide("mitt", emitter);
|
||||
app.mount("#app");
|
||||
53
src/render/preload.js
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
const { contextBridge, ipcRenderer } = require("electron");
|
||||
|
||||
/** The main <-> render interface */
|
||||
|
||||
const validFromMainChannels = [
|
||||
"set-account",
|
||||
"message",
|
||||
"ws-status",
|
||||
"record",
|
||||
"playback"
|
||||
];
|
||||
|
||||
const validToMain = [
|
||||
"messenger",
|
||||
"message",
|
||||
"login",
|
||||
"logout",
|
||||
"encode",
|
||||
"decode"
|
||||
]
|
||||
|
||||
ipcRenderer.setMaxListeners(250);
|
||||
|
||||
// Expose protected methods that allow the renderer process to use
|
||||
// the ipcRenderer without exposing the entire object
|
||||
// TODO: Implement argument filtering for added security
|
||||
contextBridge.exposeInMainWorld(
|
||||
"mainApi", {
|
||||
invoke: async (channel, ...args) => {
|
||||
if (validToMain.includes(channel)) {
|
||||
try {
|
||||
const res = await ipcRenderer.invoke(channel, ...args);
|
||||
return res;
|
||||
} catch (e) {
|
||||
console.log("IPCRenderer.invoke error");
|
||||
}
|
||||
}
|
||||
},
|
||||
send: (channel, ...args) => {
|
||||
if (validToMain.includes(channel)) {
|
||||
ipcRenderer.send(channel, ...args);
|
||||
}
|
||||
},
|
||||
on: (channel, func) => {
|
||||
if (validFromMainChannels.includes(channel)) {
|
||||
ipcRenderer.on(channel, (event, ...args) => func(...args));
|
||||
}
|
||||
},
|
||||
removeAllListeners: (channel) => {
|
||||
ipcRenderer.removeAllListeners(channel);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
// All of the Node.js APIs are available in the preload process.
|
||||
// It has the same sandbox as a Chrome extension.
|
||||
import { ipcRenderer } from "electron";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
ipcRenderer: typeof ipcRenderer;
|
||||
}
|
||||
}
|
||||
|
||||
window.ipcRenderer = ipcRenderer;
|
||||
|
||||
process.once("loaded", () => {
|
||||
|
||||
window.addEventListener("message", event => {
|
||||
|
||||
// do something with custom event
|
||||
const message = event.data;
|
||||
|
||||
if (message.endpoint === "update-menu-bar") {
|
||||
ipcRenderer.send("update-menu-bar", message.content);
|
||||
}
|
||||
|
||||
if (message.endpoint === "update-recorder") {
|
||||
ipcRenderer.send("update-recorder", message.content);
|
||||
}
|
||||
|
||||
if (message.endpoint === "client-message") {
|
||||
ipcRenderer.send("client-message", message.content);
|
||||
}
|
||||
|
||||
if (message.endpoint === "logout") {
|
||||
ipcRenderer.send("logout", message.content);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
13
src/render/shims-vue.d.ts
vendored
|
|
@ -1,13 +0,0 @@
|
|||
declare module "*.vue" {
|
||||
import { defineComponent } from "vue";
|
||||
const component: ReturnType<typeof defineComponent>;
|
||||
export default component;
|
||||
}
|
||||
|
||||
declare module "anime-js" {
|
||||
namespace anime {
|
||||
import * as anime from "animejs/lib/anime.es.js";
|
||||
function anime(): void;
|
||||
export = anime;
|
||||
}
|
||||
}
|
||||
15872
src/render/vendor/vue.js
vendored
Normal file
97
src/session.js
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
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 messageQueue = [];
|
||||
|
||||
let processContentId;
|
||||
|
||||
/* Possible messages:
|
||||
* - New UI (set ui)
|
||||
* - New message (add message to ui.messages)
|
||||
* - New content (add content to existing message)
|
||||
* - Update person
|
||||
*/
|
||||
backgroundMitt.on("message", (message) => messageQueue.push(message));
|
||||
|
||||
function handleMessageQueue()
|
||||
{
|
||||
if (!streamState.pb && !streamState.rec)
|
||||
{
|
||||
const update = messageQueue.shift();
|
||||
|
||||
if (update)
|
||||
{
|
||||
console.log("Handling UI Update:", update);
|
||||
|
||||
if (update.person)
|
||||
{
|
||||
ui = update;
|
||||
}
|
||||
else if (update.name)
|
||||
{
|
||||
ui.person = update;
|
||||
}
|
||||
else if (update.content)
|
||||
{
|
||||
ui.messages.push(update);
|
||||
}
|
||||
else
|
||||
{
|
||||
ui.messages[ui.messages.length - 1].content.push(update);
|
||||
}
|
||||
|
||||
ipcEmit("message", update);
|
||||
|
||||
// notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function notify()
|
||||
{
|
||||
const message = ui.messages[ui.messages.length - 1];
|
||||
|
||||
const content = message.content[message.content.length -1];
|
||||
|
||||
new Notification(
|
||||
{
|
||||
title: message.context,
|
||||
body: content.text
|
||||
}
|
||||
).show();
|
||||
|
||||
if (content.category == "audio/L16") playback(content.blob, message.id);
|
||||
}
|
||||
|
||||
function launchSession(account)
|
||||
{
|
||||
connectToPlatform(account);
|
||||
// initAudio();
|
||||
processContentId = setInterval(handleMessageQueue, 100);
|
||||
}
|
||||
|
||||
function endSession()
|
||||
{
|
||||
clearInterval(processContentId);
|
||||
// terminateAudio();
|
||||
disconnectFromPlatform();
|
||||
ui = null;
|
||||
}
|
||||
|
||||
ipcMain.on("messenger", () => {
|
||||
if (ui) {
|
||||
ipcEmit("message", ui);
|
||||
}
|
||||
});
|
||||
|
||||
exports.launchSession = launchSession;
|
||||
exports.endSession = endSession;
|
||||
|
||||
// ui.messages[ui.messages.findIndex(m => m.id === message.id)].content.push(message);
|
||||
101
src/session.ts
|
|
@ -1,101 +0,0 @@
|
|||
|
||||
|
||||
|
||||
|
||||
// import useAudio from "@/audio";
|
||||
import { ipcEmit } from "@/composables/useEmitter";
|
||||
import useWebsockets from "./composables/useWebsockets";
|
||||
import {config} from "@/config";
|
||||
|
||||
/* data structure of messages that's tied to the UI */
|
||||
const uiState: any | null = null;
|
||||
|
||||
/* start and stop audio functionality */
|
||||
// const { initAudio, closeAudio } = useAudio();
|
||||
|
||||
const isInitMessage = (message: any): boolean => {
|
||||
return true;
|
||||
};
|
||||
|
||||
const deauthenticate = (): void => {
|
||||
console.log('deauthenticating')
|
||||
};
|
||||
|
||||
const isAddMessage = (message: any): boolean => {
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Controls for interfacing with the platform.
|
||||
* Takes an onMessage callback which we define below.
|
||||
*/
|
||||
|
||||
|
||||
const onMessageCallback = (payload: string) => {
|
||||
|
||||
const message = JSON.parse(payload);
|
||||
console.log(typeof message);
|
||||
|
||||
/* if the platform fails to authenticate, we must back down */
|
||||
if (message === "CLOSE_AUTH_FAIL") {
|
||||
deauthenticate();
|
||||
return;
|
||||
}
|
||||
|
||||
ipcEmit("add-message", message)
|
||||
return
|
||||
|
||||
/* on init, platform sends state, used to init canvas */
|
||||
if (isInitMessage(message)) {
|
||||
ipcEmit("init-messages", message)
|
||||
}
|
||||
|
||||
|
||||
else if (isAddMessage(message)) {
|
||||
ipcEmit("add-messages", message)
|
||||
}
|
||||
|
||||
else {
|
||||
ipcEmit("update-messages", message)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const onConnectionStatusCallback = (alive: boolean) => {
|
||||
// console.log('[Session]: Connection Alive: ', alive);
|
||||
// ipcEmit('connection-state', alive);
|
||||
}
|
||||
|
||||
const { connect, send, close } = useWebsockets(
|
||||
onMessageCallback,
|
||||
onConnectionStatusCallback
|
||||
);
|
||||
|
||||
/* send a message to the platform */
|
||||
export function sendMessage<Message>(message: Message): void {
|
||||
|
||||
/* socket send */
|
||||
send(message);
|
||||
|
||||
}
|
||||
|
||||
|
||||
/* launch a new session (the main process for authenticated users) */
|
||||
export function launchSession(platformKey: string) {
|
||||
|
||||
/* connect to the platform */
|
||||
connect(config.PLATFORM_URL, platformKey);
|
||||
|
||||
/* initialize the audio streams */
|
||||
// initAudio();
|
||||
|
||||
}
|
||||
|
||||
|
||||
export function endSession() {
|
||||
|
||||
// closeAudio();
|
||||
|
||||
close();
|
||||
|
||||
}
|
||||
31
src/store.ts
|
|
@ -1,31 +0,0 @@
|
|||
const Store = require('electron-store');
|
||||
|
||||
const schema = {
|
||||
token: {
|
||||
type: 'string',
|
||||
},
|
||||
profile: {}
|
||||
};
|
||||
|
||||
const store = new Store({
|
||||
schema,
|
||||
encryptionKey: "super user test"
|
||||
});
|
||||
|
||||
export const getToken = (): string | undefined => (store.get("token"));
|
||||
|
||||
export const clearToken = (): void => (store.delete("token"));
|
||||
|
||||
export const setToken = (token: string): void => (store.set('token', token));
|
||||
|
||||
export const setProfile = (profile: Profile): Profile => (store.set('profile', profile));
|
||||
|
||||
export const getProfile = (): Profile => (store.get('profile'));
|
||||
|
||||
export const clearProfile = (): void => (store.delete('profile'));
|
||||
|
||||
export const clearStore = (): void => {
|
||||
clearToken();
|
||||
clearProfile();
|
||||
}
|
||||
|
||||
44
src/tray.js
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { Tray } = require("electron");
|
||||
|
||||
const { backgroundMitt } = require("./utils/emitter");
|
||||
|
||||
let tray;
|
||||
const TRAY_PATH = "./src/assets/tray/";
|
||||
|
||||
/* Current state of recording, playback, and ws connection */
|
||||
const state = {
|
||||
recording: false,
|
||||
playback: false,
|
||||
disconnect: 0 // whether we're experiencing connection issues
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the tray icon given a new state of an attribute.
|
||||
* @param {"recording" | "playback" | "disconnect"} attribute
|
||||
* @param {boolean} newState
|
||||
*/
|
||||
function updateTray(attribute, newState) {
|
||||
|
||||
state[attribute] = newState;
|
||||
|
||||
// convert boolean to number
|
||||
const bi = (b) => b ? 1 : 0;
|
||||
|
||||
// Make sure we have the icon we're looking for.
|
||||
tray.setImage(
|
||||
TRAY_PATH +
|
||||
`${bi(state.recording)}${bi(state.playback)}${bi(state.disconnect)}` +
|
||||
".png"
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
function initTray()
|
||||
{
|
||||
tray = new Tray(TRAY_PATH + "000" + ".png");
|
||||
}
|
||||
|
||||
exports.initTray = initTray;
|
||||
exports.updateTray = updateTray;
|
||||
77
src/types.ts
|
|
@ -1,77 +0,0 @@
|
|||
|
||||
interface Message {
|
||||
text: boolean | string;
|
||||
context: boolean | string;
|
||||
audio: boolean | string;
|
||||
type: 1 | 2 | 3;
|
||||
uid: string;
|
||||
}
|
||||
|
||||
interface ViewMessage extends Message {
|
||||
child: string;
|
||||
}
|
||||
|
||||
interface WindowState {
|
||||
width: number;
|
||||
height: number;
|
||||
x: number | null;
|
||||
y: number | null;
|
||||
}
|
||||
|
||||
interface Profile {
|
||||
crimataId: string;
|
||||
alias: string;
|
||||
initials: string;
|
||||
}
|
||||
|
||||
interface LoginPayload {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
interface AuthState {
|
||||
profile: Profile | null;
|
||||
token: string | null;
|
||||
}
|
||||
|
||||
interface AccountCredentials {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
/*
|
||||
* Electron Ipc
|
||||
*/
|
||||
interface IpcHandlerCallback<I, O> {
|
||||
(payload: I | null): Promise<Error | O>;
|
||||
}
|
||||
|
||||
interface IpcListenerCallback<T> {
|
||||
(payload: T | null): void;
|
||||
}
|
||||
|
||||
interface IIpcHandler<I, O> {
|
||||
handle(): void;
|
||||
remove(): void;
|
||||
readonly _handlerCallback: IpcHandlerCallback<I, O>;
|
||||
}
|
||||
|
||||
interface IIpcListener<I> {
|
||||
listen(): void;
|
||||
remove(): void;
|
||||
readonly _listenerCallback: IpcListenerCallback<I>;
|
||||
}
|
||||
|
||||
interface IPCHandlers {
|
||||
[handler: string]: IIpcHandler<any, any>;
|
||||
}
|
||||
|
||||
interface IPCListeners {
|
||||
[listener: string]: IIpcListener<any> | null;
|
||||
}
|
||||
|
||||
interface IpcRendererEvent<T> {
|
||||
channel: string;
|
||||
payload: T | null;
|
||||
}
|
||||
|
||||
11
src/utils/emitter.js
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
const EventEmitter = require("events");
|
||||
|
||||
const backgroundMitt = new EventEmitter();
|
||||
|
||||
const ipcEmit = (channel, ...args) =>
|
||||
{
|
||||
backgroundMitt.emit("ipc-renderer", channel, ...args);
|
||||
};
|
||||
|
||||
module.exports.backgroundMitt = backgroundMitt;
|
||||
module.exports.ipcEmit = ipcEmit;
|
||||