beginnings of refactor
This commit is contained in:
parent
ac8f338d75
commit
3b5da6124f
63 changed files with 1697 additions and 2656 deletions
213
src/App.vue
213
src/App.vue
|
|
@ -1,213 +0,0 @@
|
|||
<template>
|
||||
<!-- Only render when profile has been set -->
|
||||
<div id="app" v-if="(ready)">
|
||||
|
||||
<button id="titlebar" />
|
||||
|
||||
<!-- Minimize and exit buttons. -->
|
||||
<span class="menu">
|
||||
<button
|
||||
class="menuButton exitButton"
|
||||
@click.prevent="post('nav-bar', 'close')"
|
||||
/>
|
||||
<button
|
||||
class="menuButton minimizeButton"
|
||||
@click.prevent="post('nav-bar', 'min')"
|
||||
/>
|
||||
</span>
|
||||
|
||||
<!-- Main Pages -->
|
||||
<Messenger
|
||||
v-if="profile"
|
||||
:profile="profile"
|
||||
:newMessages="newMessages"
|
||||
/>
|
||||
<Login v-else />
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Show spash screen if ready is false -->
|
||||
<Splash v-else />
|
||||
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, onMounted, onUnmounted, ref } from "vue";
|
||||
import { IpcRendererEvent } from "electron";
|
||||
import { useIpc } from "@/modules/ipc";
|
||||
import { useProfile } from "@/modules/auth"
|
||||
import { invokeProfile } from "@/ipcRend/account";
|
||||
import { postInitSession, postMount } from "@/ipcRend/session";
|
||||
|
||||
import Splash from "@/components/splash.vue";
|
||||
import Messenger from "@/components/messenger.vue";
|
||||
import Login from "@/components/login.vue";
|
||||
|
||||
import { Profile } from "@/types";
|
||||
|
||||
|
||||
export default defineComponent({
|
||||
|
||||
components: {
|
||||
Splash,
|
||||
Messenger,
|
||||
Login
|
||||
},
|
||||
|
||||
setup() {
|
||||
|
||||
const { post, invoke } = useIpc();
|
||||
|
||||
const { profile, setProfile, clearProfile } = useProfile();
|
||||
|
||||
// Whether browser has received user info yet.
|
||||
const ready = ref(false);
|
||||
|
||||
// New messages that browser missed while closed.
|
||||
const newMessages = ref([]);
|
||||
|
||||
// Receive updated information about the session.
|
||||
const updateState = (_event: IpcRendererEvent, payload: any) => {
|
||||
console.log('YAYAYAYAYA');
|
||||
console.log("APP:Received updated profile and new messages: \n" +
|
||||
` profile: ${payload.message.profile}\n` +
|
||||
` new: ${payload.message.newMessages}`);
|
||||
|
||||
if (payload.message.profile) {
|
||||
console.log(`APP:Logged-in, showing Messenger View.`);
|
||||
} else {
|
||||
console.log(`APP:Logged-out, showing Login View.`);
|
||||
}
|
||||
|
||||
newMessages.value = payload.message.newMessages;
|
||||
|
||||
ready.value = true;
|
||||
|
||||
}
|
||||
|
||||
const onSessionAuthFail = (_event: IpcRendererEvent, _payload: null) => {
|
||||
clearProfile();
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
console.log("APP:mounted.");
|
||||
|
||||
window.ipcRenderer.on("session-auth-fail", onSessionAuthFail)
|
||||
window.ipcRenderer.on("update-state", updateState)
|
||||
window.ipcRenderer.on("update_available", () => {
|
||||
console.log('testing auto update');
|
||||
})
|
||||
|
||||
window.ipcRenderer.on("update_downloaded", () => {
|
||||
console.log('testing update download');
|
||||
})
|
||||
|
||||
postMount();
|
||||
|
||||
try {
|
||||
|
||||
const profile = await invokeProfile() as Profile;
|
||||
setProfile(profile);
|
||||
} catch(e) {
|
||||
console.log('[AUTH]', e);
|
||||
clearProfile();
|
||||
} finally {
|
||||
ready.value = true;
|
||||
if (profile.value.crimataId) {
|
||||
// start session
|
||||
postInitSession(profile.value.crimataId);
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.ipcRenderer.removeAllListeners("update-state");
|
||||
window.ipcRenderer.removeAllListeners("session-auth-fail");
|
||||
})
|
||||
|
||||
return {
|
||||
ready,
|
||||
post,
|
||||
newMessages,
|
||||
profile
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
// Background color set in window.ts
|
||||
}
|
||||
|
||||
#app {
|
||||
font-family: "SF Pro Text";
|
||||
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
#titlebar {
|
||||
position: fixed;
|
||||
|
||||
width: 100vw;
|
||||
height: 54px;
|
||||
|
||||
opacity: 0.75;
|
||||
|
||||
background-color: #EBEBEB;
|
||||
|
||||
-webkit-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>
|
||||
|
|
@ -1,27 +1,14 @@
|
|||
|
||||
import { useHttp } from "@/modules/http";
|
||||
import { useHttp } from "@/composables/http";
|
||||
import axios from "axios";
|
||||
|
||||
const { post } = useHttp();
|
||||
|
||||
export const submit =
|
||||
async (email: string, password: string) => (
|
||||
|
||||
await post('/account/login', {
|
||||
email,
|
||||
password
|
||||
})
|
||||
|
||||
export const submit = async (email: string, password: string) => (
|
||||
await post('/account/login', { email, password })
|
||||
)
|
||||
|
||||
|
||||
export const logout =
|
||||
async (): Promise<null | Error> => (await post('/account/logout'));
|
||||
|
||||
|
||||
|
||||
export const fetchProfile = async(email: string, token: string) => (
|
||||
|
||||
export const fetchAccount = async (email: string, token: string) => (
|
||||
await axios({
|
||||
url: "http://127.0.0.1:3000/api/account/profile",
|
||||
headers: {
|
||||
|
|
|
|||
|
|
@ -1,14 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="13.795" height="12.759" viewBox="0 0 13.795 12.759">
|
||||
<g id="Group_544" data-name="Group 544" transform="translate(-4127 -507)">
|
||||
<g id="Group_33" data-name="Group 33" transform="translate(4127 507)">
|
||||
<g id="Group_32" data-name="Group 32" transform="translate(0 0)">
|
||||
<g id="Group_31" data-name="Group 31" transform="translate(0 6.627)">
|
||||
<circle id="Ellipse_50" data-name="Ellipse 50" cx="3.066" cy="3.066" r="3.066" transform="translate(0 0)" fill="#383838"/>
|
||||
<circle id="Ellipse_51" data-name="Ellipse 51" cx="3.066" cy="3.066" r="3.066" transform="translate(7.664 0)" fill="#383838"/>
|
||||
</g>
|
||||
<circle id="Ellipse_52" data-name="Ellipse 52" cx="3.066" cy="3.066" r="3.066" transform="translate(3.826 0)" fill="#383838"/>
|
||||
<path id="Path_150" data-name="Path 150" d="M36.27,83.67" transform="translate(-33.199 -73.977)" fill="#ff0"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 973 B |
0
src/audio.ts
Normal file
0
src/audio.ts
Normal file
|
|
@ -1,26 +0,0 @@
|
|||
/*
|
||||
* Entry point for Crimata electron app.
|
||||
* "Look on my Works, ye Mighty, and despair!"
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
import { initApp } from './background/init';
|
||||
import { protocol } from "electron";
|
||||
require('dotenv').config()
|
||||
|
||||
// Scheme must be registered before the app is ready
|
||||
protocol.registerSchemesAsPrivileged([
|
||||
{ scheme: "app", privileges: { secure: true, standard: true } }
|
||||
]);
|
||||
|
||||
// Load environment variable
|
||||
const isDev = require('electron-is-dev');
|
||||
|
||||
// NOTE Program Begins Here
|
||||
(() => {
|
||||
|
||||
console.log('Starting Crimata electron app.');
|
||||
initApp(isDev);
|
||||
|
||||
})();
|
||||
|
|
@ -1,172 +0,0 @@
|
|||
/* eslint @typescript-eslint/no-var-requires: "off" */
|
||||
|
||||
"use strict";
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
import fs from 'fs';
|
||||
import { backgroundMitt } from "@/modules/emitter";
|
||||
import { SessionState, WindowState } from "@/types";
|
||||
import { app } from "electron";
|
||||
|
||||
const configPath = app.getPath("userData");
|
||||
|
||||
export const ipcEmit = (channel: string, payload: any) => {
|
||||
backgroundMitt.emit('ipc-renderer', {
|
||||
endpoint: channel,
|
||||
message: payload
|
||||
});
|
||||
}
|
||||
|
||||
export const loadState = (fileName: string): SessionState => {
|
||||
let state: SessionState;
|
||||
|
||||
try {
|
||||
state = JSON.parse(fs.readFileSync(configPath + fileName).toString());
|
||||
}
|
||||
|
||||
catch (error) {
|
||||
state = {
|
||||
key: false,
|
||||
newMessages: []
|
||||
}
|
||||
}
|
||||
|
||||
return state
|
||||
|
||||
}
|
||||
|
||||
export const loadWinState = (fileName: string): WindowState => {
|
||||
let state: WindowState;
|
||||
|
||||
try {
|
||||
state = JSON.parse(fs.readFileSync(configPath + fileName).toString());
|
||||
}
|
||||
|
||||
catch (error) {
|
||||
state = {
|
||||
width: 600,
|
||||
height: 500,
|
||||
x: null,
|
||||
y: null,
|
||||
}
|
||||
}
|
||||
|
||||
return state
|
||||
|
||||
}
|
||||
|
||||
// Save session or window state.
|
||||
export const saveToJson = (fileName: string, data: any) => {
|
||||
|
||||
fs.writeFile(configPath + fileName, JSON.stringify(data), (err) => {
|
||||
if (err) {
|
||||
console.log("Error when saving to json.")
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
|
@ -1,89 +0,0 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
import { app, dialog } from "electron";
|
||||
import { createWindow } from './window';
|
||||
import { stopStream } from './audio';
|
||||
import { backgroundMitt } from '@/modules/emitter';
|
||||
import useIpc from "@/background/ipc/index";
|
||||
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()
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
|
||||
|
||||
// Run when electron app is initialized.
|
||||
async function main(): Promise<void> {
|
||||
|
||||
console.log("MAIN:Initializing Electron App.");
|
||||
|
||||
useIpc();
|
||||
|
||||
// Must wait til window is created.
|
||||
await createWindow();
|
||||
|
||||
}
|
||||
|
||||
// Root function of app.
|
||||
export function initApp(dev: boolean): void {
|
||||
|
||||
// On initial startup.
|
||||
app.on("ready", () => {
|
||||
// autoUpdater.checkForUpdates()
|
||||
main();
|
||||
});
|
||||
|
||||
// Must keep to ensure app doesn't quit on close.
|
||||
app.on("before-quit", async () => {
|
||||
await stopStream();
|
||||
});
|
||||
|
||||
// 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 (dev) {
|
||||
process.on("SIGTERM", () => {
|
||||
app.quit();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
import { ipcMain, IpcMainInvokeEvent, IpcMainEvent } from "electron";
|
||||
import { fetchAudioInput, toggleRecord } from "@/background/audio";
|
||||
|
||||
|
||||
// Toggles record to true to begin capturing chunks.
|
||||
const onRecordingStart = (
|
||||
_event: IpcMainEvent,
|
||||
_payload: null
|
||||
): void => {
|
||||
|
||||
console.log('[IPC]: start-recording');
|
||||
|
||||
toggleRecord();
|
||||
};
|
||||
|
||||
|
||||
// Returns recorded audio to frontend and sets record to false.
|
||||
const onRecordingStop = async (
|
||||
_event: IpcMainInvokeEvent,
|
||||
_payload: null
|
||||
): Promise<Error | string> => {
|
||||
|
||||
console.log('[IPC]: stop-recording');
|
||||
|
||||
return await fetchAudioInput()
|
||||
};
|
||||
|
||||
|
||||
export default function useAudioListeners(): void {
|
||||
|
||||
ipcMain.removeAllListeners("start-recording");
|
||||
ipcMain.on("start-recording", onRecordingStart);
|
||||
|
||||
ipcMain.removeHandler("stop-recording");
|
||||
ipcMain.handle("stop-recording", onRecordingStop);
|
||||
|
||||
}
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
import { initSession, emitNewMessages, sendMessage } from '@/background/session';
|
||||
import { ipcMain, IpcMainInvokeEvent, IpcMainEvent } from "electron";
|
||||
import { initAudioIO } from "@/background/audio";
|
||||
import { ClientMessage } from "@/types";
|
||||
import { store } from "@/background/store";
|
||||
|
||||
|
||||
// Instantiate socket session with crimata-platorm.
|
||||
const onSessionInit = (
|
||||
_event: IpcMainInvokeEvent,
|
||||
cid: string
|
||||
): void => {
|
||||
|
||||
console.log('[IPC]: init-session');
|
||||
|
||||
const token = store.get('key');
|
||||
const crimataId = store.get('crimataId');
|
||||
|
||||
|
||||
initSession({
|
||||
token,
|
||||
crimataId
|
||||
});
|
||||
|
||||
initAudioIO();
|
||||
}
|
||||
|
||||
|
||||
const onAppMounted = (
|
||||
_event: IpcMainInvokeEvent,
|
||||
_payload: null
|
||||
): void => {
|
||||
|
||||
console.log('[IPC]: app-mounted');
|
||||
|
||||
emitNewMessages()
|
||||
};
|
||||
|
||||
|
||||
// Handle messages from window/client.
|
||||
const onClientMessage = (
|
||||
_event: IpcMainEvent,
|
||||
payload: ClientMessage
|
||||
): void => {
|
||||
|
||||
console.log('[IPC]: client-message');
|
||||
|
||||
sendMessage(payload);
|
||||
}
|
||||
|
||||
|
||||
export default function useSessionListeners(): void {
|
||||
|
||||
console.log('[IPC]: Init session listeners.');
|
||||
|
||||
// Attach listeners for frontend.
|
||||
ipcMain.removeAllListeners("client-message");
|
||||
ipcMain.on("client-message", onClientMessage);
|
||||
|
||||
// Attack browser window init listener.
|
||||
ipcMain.removeAllListeners("app-mounted");
|
||||
ipcMain.on("app-mounted", onAppMounted);
|
||||
|
||||
ipcMain.removeAllListeners("init-session");
|
||||
ipcMain.on("init-session", onSessionInit);
|
||||
|
||||
}
|
||||
|
|
@ -1,121 +0,0 @@
|
|||
/*
|
||||
* Creates a websocket session with Crimata Servers.
|
||||
*
|
||||
* Connects to Servers and attempts key authentication. Server will respond
|
||||
* with key and user profile. We send the profile to the browser. We also
|
||||
* resend this information on new broser window. We then serve as a
|
||||
* communication interface between the window and the servers. It will
|
||||
* automatically try to reconnect on websocket disconnect.
|
||||
*/
|
||||
|
||||
import { backgroundMitt } from "@/modules/emitter";
|
||||
import { ipcEmit, loadState } from './helpers';
|
||||
import WebSocket from 'ws';
|
||||
|
||||
import useWebSockets from "./websockets";
|
||||
|
||||
import { play } from "./audio";
|
||||
import { renderMessage } from "@/modules/message";
|
||||
import { SessionState } from "@/types";
|
||||
|
||||
let win = true;
|
||||
|
||||
// Info saved to json on quit (key, newMessages).
|
||||
let state: SessionState;
|
||||
|
||||
let socket: WebSocket | null = null;
|
||||
|
||||
|
||||
// Calls appropriate endpoint for a server message.
|
||||
const onMessage = (data: string): void => {
|
||||
let message = JSON.parse(data);
|
||||
if (message === "CLOSE_AUTH_FAIL") {
|
||||
ipcEmit("session-auth-fail", null)
|
||||
return;
|
||||
}
|
||||
|
||||
// Standard message.
|
||||
if (message.content) {
|
||||
|
||||
// Convert to render message
|
||||
message = renderMessage(
|
||||
message.content.text,
|
||||
message.content.audio,
|
||||
message.context,
|
||||
message.modifier
|
||||
);
|
||||
|
||||
if (win) {
|
||||
console.log("SESS:Emitting standard message.")
|
||||
if (message.audio) {
|
||||
play(message.audio)
|
||||
}
|
||||
ipcEmit("render-message", message)
|
||||
}
|
||||
|
||||
else {
|
||||
console.log("SESS:No window: saving message.")
|
||||
state.newMessages.push(message);
|
||||
}
|
||||
}
|
||||
|
||||
else {
|
||||
if (win) {
|
||||
ipcEmit("render-message", message)
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
// Websockets module.
|
||||
const { createSocket, send } = useWebSockets(onMessage);
|
||||
|
||||
|
||||
export const emitNewMessages = (): void => {
|
||||
if (state) {
|
||||
ipcEmit("update-state", {
|
||||
newMessages: state.newMessages,
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
export const sendMessage = (payload: Record<string, any>): void => {
|
||||
try {
|
||||
send(payload)
|
||||
} catch(e) {
|
||||
console.log("Unable to send message: ", payload);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const endSession = (): void => {
|
||||
if (socket) {
|
||||
socket.close();
|
||||
socket = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Call this to initialize session with Crimata servers.
|
||||
export const initSession = (authPayload: {
|
||||
token: string;
|
||||
crimataId: string;
|
||||
}): void => {
|
||||
console.log("SESS:Creating new session.")
|
||||
|
||||
// Load Json or createState.
|
||||
state = loadState("session.json");
|
||||
|
||||
// Open socket connection.
|
||||
if (!socket)
|
||||
socket = createSocket(authPayload);
|
||||
|
||||
// Keep win up-to-date.
|
||||
backgroundMitt.on('window-active', (state: boolean) => {
|
||||
win = state;
|
||||
});
|
||||
|
||||
}
|
||||
|
|
@ -1,96 +0,0 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
import WebSocket from 'ws';
|
||||
|
||||
let socket: WebSocket;
|
||||
|
||||
const socketUrl = "ws://127.0.0.1:8760"
|
||||
|
||||
// Run every time we want to connect to backend.
|
||||
export default function useWebSockets(
|
||||
receiveCallback: (s: string) => void,
|
||||
openCallback?: () => void
|
||||
) {
|
||||
|
||||
// Returns bool (sucess or fail).
|
||||
const sendMessage = (data: any) => {
|
||||
console.log("WS:Sending message: ", data)
|
||||
|
||||
if (socket.readyState !== 1) {
|
||||
return false
|
||||
}
|
||||
|
||||
else {
|
||||
socket.send(JSON.stringify(data))
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const send = async (data: Record<string, any>): Promise<boolean> => (
|
||||
new Promise((resolve, reject) => {
|
||||
if (socket.readyState !== 1) {
|
||||
reject(false);
|
||||
}
|
||||
socket.send(JSON.stringify(data))
|
||||
resolve(true);
|
||||
|
||||
}))
|
||||
|
||||
|
||||
const onOpen = (_event: WebSocket.OpenEvent) => {
|
||||
|
||||
console.log("WS:Connected to WS Server!");
|
||||
if (openCallback) openCallback();
|
||||
|
||||
}
|
||||
|
||||
const onServerMessage = (event: WebSocket.MessageEvent) => {
|
||||
|
||||
console.log("WS:Message received: ", event.data);
|
||||
|
||||
receiveCallback(event.data.toString())
|
||||
|
||||
}
|
||||
|
||||
const onClose = (event: WebSocket.CloseEvent) => {
|
||||
console.log("WS:Socket closed normally.")
|
||||
}
|
||||
|
||||
// Reconnect automatically on error.
|
||||
const onError = (event: WebSocket.ErrorEvent) => {
|
||||
console.log("WS:WebSocket error: ", event.message);
|
||||
|
||||
console.log("Attempting reconnect in 1s.")
|
||||
setTimeout(createSocket, 1000)
|
||||
}
|
||||
|
||||
const createSocket = (authPayload: {
|
||||
token: string;
|
||||
crimataId: string;
|
||||
}):WebSocket => {
|
||||
socket = new WebSocket(socketUrl)
|
||||
|
||||
// Add listeners.
|
||||
socket.addEventListener("open", (_event: WebSocket.OpenEvent) => {
|
||||
socket.send(JSON.stringify({
|
||||
key: authPayload.token,
|
||||
crimata_id: authPayload.crimataId
|
||||
}));
|
||||
})
|
||||
socket.addEventListener("message", onServerMessage)
|
||||
socket.addEventListener("close", onClose)
|
||||
socket.addEventListener("error", onError)
|
||||
|
||||
return socket;
|
||||
|
||||
}
|
||||
|
||||
return {
|
||||
createSocket,
|
||||
sendMessage,
|
||||
send
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,118 +0,0 @@
|
|||
import anime from "animejs";
|
||||
import useMitt from "@/modules/mitt";
|
||||
import { useIpc } from '@/modules/ipc';
|
||||
import { onMounted, onUnmounted, ref, Ref } from "vue";
|
||||
import keyboardNameMap from "../keyBoardMaps/keyboardNameMap";
|
||||
import { renderMessage, clientMessage } from '@/modules/message';
|
||||
import { postMessage } from "@/ipcRend/session";
|
||||
import { invokeStopRecord } from "@/ipcRend/audio";
|
||||
|
||||
|
||||
function showRecIcon () {
|
||||
|
||||
anime({
|
||||
targets: '#recIcon',
|
||||
opacity: [0, 0.75],
|
||||
scale: [0.0, 1],
|
||||
duration: 250,
|
||||
easing: 'linear',
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
function hideRecIcon () {
|
||||
|
||||
anime({
|
||||
targets: '#recIcon',
|
||||
opacity: [0.75, 0],
|
||||
scale: [1, 0],
|
||||
duration: 250,
|
||||
easing: 'linear',
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
|
||||
export default function useAudioInputController (typing: Ref) {
|
||||
|
||||
// For sending messages.
|
||||
const { post, invoke } = useIpc();
|
||||
const { emitter } = useMitt();
|
||||
|
||||
// Keepp track of when we are recording.
|
||||
const recording = ref(false);
|
||||
|
||||
//---Callbacks-----------------------------------------------
|
||||
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
const cmd = keyboardNameMap[e.keyCode];
|
||||
// console.log(cmd)
|
||||
|
||||
// Start recording on space bar.
|
||||
if (cmd == "SPACE" && !recording.value && !typing.value) {
|
||||
|
||||
console.log("INPT:Starting record.")
|
||||
post("start-recording", null);
|
||||
|
||||
showRecIcon()
|
||||
recording.value = true;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const onKeyUp = async (e: KeyboardEvent) => {
|
||||
const cmd = keyboardNameMap[e.keyCode];
|
||||
|
||||
// Stop recording on space up.
|
||||
if (cmd == "SPACE" && recording.value) {
|
||||
|
||||
// Create a message.
|
||||
const message = renderMessage(
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"sf"
|
||||
)
|
||||
|
||||
// Render it immediately.
|
||||
emitter.emit("self-message", message);
|
||||
|
||||
// Stop recording and get audio from recorder.
|
||||
console.log("INPT:Stopping record.")
|
||||
try {
|
||||
const audio = await invokeStopRecord() as string;
|
||||
// Send message to the backend for processing.
|
||||
const clientM = clientMessage("", audio, message.uid);
|
||||
postMessage(clientM);
|
||||
} catch(e) {
|
||||
console.log('Failed to fetch audio.')
|
||||
} finally {
|
||||
hideRecIcon();
|
||||
recording.value = false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener("keydown", onKeyDown);
|
||||
window.addEventListener("keyup", onKeyUp);
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener("keydown", onKeyDown);
|
||||
window.removeEventListener("keyup", onKeyUp);
|
||||
});
|
||||
|
||||
|
||||
return {
|
||||
recording
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,263 +0,0 @@
|
|||
// This has the UnShifted and Shifted characters that each key maps to
|
||||
// Ones that are to be ignored for character input are empty.
|
||||
const keyboardCharMap = [
|
||||
["", ""], // [0]
|
||||
["", ""], // [1]
|
||||
["", ""], // [2]
|
||||
["", ""], // [3]
|
||||
["", ""], // [4]
|
||||
["", ""], // [5]
|
||||
["", ""], // [6]
|
||||
["", ""], // [7]
|
||||
["", ""], // [8]
|
||||
["", ""], // [9]
|
||||
["", ""], // [10]
|
||||
["", ""], // [11]
|
||||
["", ""], // [12]
|
||||
["\r", "\r"], // [13] - MOST control characters are ignored. This one (Carriage Return, or "Enter") is significant!
|
||||
["", ""], // [14]
|
||||
["", ""], // [15]
|
||||
["", ""], // [16]
|
||||
["", ""], // [17]
|
||||
["", ""], // [18]
|
||||
["", ""], // [19]
|
||||
["", ""], // [20]
|
||||
["", ""], // [21]
|
||||
["", ""], // [22]
|
||||
["", ""], // [23]
|
||||
["", ""], // [24]
|
||||
["", ""], // [25]
|
||||
["", ""], // [26]
|
||||
["", ""], // [27]
|
||||
["", ""], // [28]
|
||||
["", ""], // [29]
|
||||
["", ""], // [30]
|
||||
["", ""], // [31]
|
||||
[" ", " "], // [32] // SPACE! Don't "clean it up" and remove the space!
|
||||
["", ""], // [33]
|
||||
["", ""], // [34]
|
||||
["", ""], // [35]
|
||||
["", ""], // [36]
|
||||
["", ""], // [37]
|
||||
["", ""], // [38]
|
||||
["", ""], // [39]
|
||||
["", ""], // [40]
|
||||
["", ""], // [41]
|
||||
["", ""], // [42]
|
||||
["", ""], // [43]
|
||||
["", ""], // [44]
|
||||
["", ""], // [45]
|
||||
["", ""], // [46]
|
||||
["", ""], // [47]
|
||||
["0", ")"], // [48]
|
||||
["1", "!"], // [49]
|
||||
["2", "@"], // [50]
|
||||
["3", "#"], // [51]
|
||||
["4", "$"], // [52]
|
||||
["5", "%"], // [53]
|
||||
["6", "^"], // [54]
|
||||
["7", "&"], // [55]
|
||||
["8", "*"], // [56]
|
||||
["9", "("], // [57]
|
||||
["", ""], // [58]
|
||||
[";", ":"], // [59]
|
||||
["<", ""], // [60]
|
||||
["=", ""], // [61]
|
||||
[">", ""], // [62]
|
||||
["?", ""], // [63] shifted; else "/"
|
||||
["", ""], // [64]
|
||||
["a", "A"], // [65]
|
||||
["b", "B"], // [66]
|
||||
["c", "C"], // [67]
|
||||
["d", "D"], // [68]
|
||||
["e", "E"], // [69]
|
||||
["f", "F"], // [70]
|
||||
["g", "G"], // [71]
|
||||
["h", "H"], // [72]
|
||||
["i", "I"], // [73]
|
||||
["j", "J"], // [74]
|
||||
["k", "K"], // [75]
|
||||
["l", "L"], // [76]
|
||||
["m", "M"], // [77]
|
||||
["n", "N"], // [78]
|
||||
["o", "O"], // [79]
|
||||
["p", "P"], // [80]
|
||||
["q", "Q"], // [81]
|
||||
["r", "R"], // [82]
|
||||
["s", "S"], // [83]
|
||||
["t", "T"], // [84]
|
||||
["u", "U"], // [85]
|
||||
["v", "V"], // [86]
|
||||
["w", "W"], // [87]
|
||||
["x", "X"], // [88]
|
||||
["y", "Y"], // [89]
|
||||
["z", "Z"], // [90]
|
||||
["", ""], // [91] Windows Key (Windows) or Command Key (Mac)
|
||||
["", ""], // [92]
|
||||
["", ""], // [93]
|
||||
["", ""], // [94]
|
||||
["", ""], // [95]
|
||||
// Number Keypad Entries...
|
||||
["0", ""], // [96]
|
||||
["1", ""], // [97]
|
||||
["2", ""], // [98]
|
||||
["3", ""], // [99]
|
||||
["4", ""], // [100]
|
||||
["5", ""], // [101]
|
||||
["6", ""], // [102]
|
||||
["7", ""], // [103]
|
||||
["8", ""], // [104]
|
||||
["9", ""], // [105]
|
||||
["*", ""], // [106]
|
||||
["+", ""], // [107]
|
||||
["", ""], // [108]
|
||||
["-", ""], // [109]
|
||||
[".", ""], // [110]
|
||||
["/", ""], // [111]
|
||||
|
||||
["", ""], // [112]
|
||||
["", ""], // [113]
|
||||
["", ""], // [114]
|
||||
["", ""], // [115]
|
||||
["", ""], // [116]
|
||||
["", ""], // [117]
|
||||
["", ""], // [118]
|
||||
["", ""], // [119]
|
||||
["", ""], // [120]
|
||||
["", ""], // [121]
|
||||
["", ""], // [122]
|
||||
["", ""], // [123]
|
||||
["", ""], // [124]
|
||||
["", ""], // [125]
|
||||
["", ""], // [126]
|
||||
["", ""], // [127]
|
||||
["", ""], // [128]
|
||||
["", ""], // [129]
|
||||
["", ""], // [130]
|
||||
["", ""], // [131]
|
||||
["", ""], // [132]
|
||||
["", ""], // [133]
|
||||
["", ""], // [134]
|
||||
["", ""], // [135]
|
||||
["", ""], // [136]
|
||||
["", ""], // [137]
|
||||
["", ""], // [138]
|
||||
["", ""], // [139]
|
||||
["", ""], // [140]
|
||||
["", ""], // [141]
|
||||
["", ""], // [142]
|
||||
["", ""], // [143]
|
||||
["", ""], // [144]
|
||||
["", ""], // [145]
|
||||
["", ""], // [146]
|
||||
["", ""], // [147]
|
||||
["", ""], // [148]
|
||||
["", ""], // [149]
|
||||
["", ""], // [150]
|
||||
["", ""], // [151]
|
||||
["", ""], // [152]
|
||||
["", ""], // [153]
|
||||
["", ""], // [154]
|
||||
["", ""], // [155]
|
||||
["", ""], // [156]
|
||||
["", ""], // [157]
|
||||
["", ""], // [158]
|
||||
["", ""], // [159]
|
||||
["", ""], // [160]
|
||||
["", ""], // [161]
|
||||
["", ""], // [162]
|
||||
["", ""], // [163]
|
||||
["", ""], // [164]
|
||||
["", ""], // [165]
|
||||
["", ""], // [166]
|
||||
["", ""], // [167]
|
||||
["", ""], // [168]
|
||||
["", ""], // [169]
|
||||
["", ""], // [170]
|
||||
["", ""], // [171]
|
||||
["", ""], // [172]
|
||||
["", ""], // [173]
|
||||
["", ""], // [174]
|
||||
["", ""], // [175]
|
||||
["", ""], // [176]
|
||||
["", ""], // [177]
|
||||
["", ""], // [178]
|
||||
["", ""], // [179]
|
||||
["", ""], // [180]
|
||||
["", ""], // [181]
|
||||
["", ""], // [182]
|
||||
["", ""], // [183]
|
||||
["", ""], // [184]
|
||||
["", ""], // [185]
|
||||
[";", ":"], // [186]
|
||||
["=", "+"], // [187]
|
||||
[",", "<"], // [188]
|
||||
["-", "_"], // [189]
|
||||
[".", ">"], // [190]
|
||||
["/", "?"], // [191]
|
||||
["`", "~"], // [192]
|
||||
["", ""], // [193]
|
||||
["", ""], // [194]
|
||||
["", ""], // [195]
|
||||
["", ""], // [196]
|
||||
["", ""], // [197]
|
||||
["", ""], // [198]
|
||||
["", ""], // [199]
|
||||
["", ""], // [200]
|
||||
["", ""], // [201]
|
||||
["", ""], // [202]
|
||||
["", ""], // [203]
|
||||
["", ""], // [204]
|
||||
["", ""], // [205]
|
||||
["", ""], // [206]
|
||||
["", ""], // [207]
|
||||
["", ""], // [208]
|
||||
["", ""], // [209]
|
||||
["", ""], // [210]
|
||||
["", ""], // [211]
|
||||
["", ""], // [212]
|
||||
["", ""], // [213]
|
||||
["", ""], // [214]
|
||||
["", ""], // [215]
|
||||
["", ""], // [216]
|
||||
["", ""], // [217]
|
||||
["", ""], // [218]
|
||||
["[", "{"], // [219]
|
||||
["\\", "|"], // [220]
|
||||
["]", "}"], // [221]
|
||||
["'", '"'], // [222]
|
||||
["", ""], // [223]
|
||||
["", ""], // [224]
|
||||
["", ""], // [225]
|
||||
["", ""], // [226]
|
||||
["", ""], // [227]
|
||||
["", ""], // [228]
|
||||
["", ""], // [229]
|
||||
["", ""], // [230]
|
||||
["", ""], // [231]
|
||||
["", ""], // [232]
|
||||
["", ""], // [233]
|
||||
["", ""], // [234]
|
||||
["", ""], // [235]
|
||||
["", ""], // [236]
|
||||
["", ""], // [237]
|
||||
["", ""], // [238]
|
||||
["", ""], // [239]
|
||||
["", ""], // [240]
|
||||
["", ""], // [241]
|
||||
["", ""], // [242]
|
||||
["", ""], // [243]
|
||||
["", ""], // [244]
|
||||
["", ""], // [245]
|
||||
["", ""], // [246]
|
||||
["", ""], // [247]
|
||||
["", ""], // [248]
|
||||
["", ""], // [249]
|
||||
["", ""], // [250]
|
||||
["", ""], // [251]
|
||||
["", ""], // [252]
|
||||
["", ""], // [253]
|
||||
["", ""], // [254]
|
||||
["", ""] // [255]
|
||||
];
|
||||
export default keyboardCharMap;
|
||||
|
|
@ -1,261 +0,0 @@
|
|||
// names of known key codes (0-255)
|
||||
const keyboardNameMap = [
|
||||
"", // [0]
|
||||
"", // [1]
|
||||
"", // [2]
|
||||
"CANCEL", // [3]
|
||||
"", // [4]
|
||||
"", // [5]
|
||||
"HELP", // [6]
|
||||
"", // [7]
|
||||
"BACK_SPACE", // [8]
|
||||
"TAB", // [9]
|
||||
"", // [10]
|
||||
"", // [11]
|
||||
"CLEAR", // [12]
|
||||
"ENTER", // [13]
|
||||
"ENTER_SPECIAL", // [14]
|
||||
"", // [15]
|
||||
"SHIFT", // [16]
|
||||
"CONTROL", // [17]
|
||||
"ALT", // [18]
|
||||
"PAUSE", // [19]
|
||||
"CAPS_LOCK", // [20]
|
||||
"KANA", // [21]
|
||||
"EISU", // [22]
|
||||
"JUNJA", // [23]
|
||||
"FINAL", // [24]
|
||||
"HANJA", // [25]
|
||||
"", // [26]
|
||||
"ESCAPE", // [27]
|
||||
"CONVERT", // [28]
|
||||
"NONCONVERT", // [29]
|
||||
"ACCEPT", // [30]
|
||||
"MODECHANGE", // [31]
|
||||
"SPACE", // [32]
|
||||
"PAGE_UP", // [33]
|
||||
"PAGE_DOWN", // [34]
|
||||
"END", // [35]
|
||||
"HOME", // [36]
|
||||
"LEFT", // [37]
|
||||
"UP", // [38]
|
||||
"RIGHT", // [39]
|
||||
"DOWN", // [40]
|
||||
"SELECT", // [41]
|
||||
"PRINT", // [42]
|
||||
"EXECUTE", // [43]
|
||||
"PRINTSCREEN", // [44]
|
||||
"INSERT", // [45]
|
||||
"DELETE", // [46]
|
||||
"", // [47]
|
||||
"0", // [48]
|
||||
"1", // [49]
|
||||
"2", // [50]
|
||||
"3", // [51]
|
||||
"4", // [52]
|
||||
"5", // [53]
|
||||
"6", // [54]
|
||||
"7", // [55]
|
||||
"8", // [56]
|
||||
"9", // [57]
|
||||
"COLON", // [58]
|
||||
"SEMICOLON", // [59]
|
||||
"LESS_THAN", // [60]
|
||||
"EQUALS", // [61]
|
||||
"GREATER_THAN", // [62]
|
||||
"QUESTION_MARK", // [63]
|
||||
"AT", // [64]
|
||||
"A", // [65]
|
||||
"B", // [66]
|
||||
"C", // [67]
|
||||
"D", // [68]
|
||||
"E", // [69]
|
||||
"F", // [70]
|
||||
"G", // [71]
|
||||
"H", // [72]
|
||||
"I", // [73]
|
||||
"J", // [74]
|
||||
"K", // [75]
|
||||
"L", // [76]
|
||||
"M", // [77]
|
||||
"N", // [78]
|
||||
"O", // [79]
|
||||
"P", // [80]
|
||||
"Q", // [81]
|
||||
"R", // [82]
|
||||
"S", // [83]
|
||||
"T", // [84]
|
||||
"U", // [85]
|
||||
"V", // [86]
|
||||
"W", // [87]
|
||||
"X", // [88]
|
||||
"Y", // [89]
|
||||
"Z", // [90]
|
||||
"OS_KEY", // [91] Windows Key (Windows) or Command Key (Mac)
|
||||
"", // [92]
|
||||
"CONTEXT_MENU", // [93]
|
||||
"", // [94]
|
||||
"SLEEP", // [95]
|
||||
"NUMPAD0", // [96]
|
||||
"NUMPAD1", // [97]
|
||||
"NUMPAD2", // [98]
|
||||
"NUMPAD3", // [99]
|
||||
"NUMPAD4", // [100]
|
||||
"NUMPAD5", // [101]
|
||||
"NUMPAD6", // [102]
|
||||
"NUMPAD7", // [103]
|
||||
"NUMPAD8", // [104]
|
||||
"NUMPAD9", // [105]
|
||||
"MULTIPLY", // [106]
|
||||
"ADD", // [107]
|
||||
"SEPARATOR", // [108]
|
||||
"SUBTRACT", // [109]
|
||||
"DECIMAL", // [110]
|
||||
"DIVIDE", // [111]
|
||||
"F1", // [112]
|
||||
"F2", // [113]
|
||||
"F3", // [114]
|
||||
"F4", // [115]
|
||||
"F5", // [116]
|
||||
"F6", // [117]
|
||||
"F7", // [118]
|
||||
"F8", // [119]
|
||||
"F9", // [120]
|
||||
"F10", // [121]
|
||||
"F11", // [122]
|
||||
"F12", // [123]
|
||||
"F13", // [124]
|
||||
"F14", // [125]
|
||||
"F15", // [126]
|
||||
"F16", // [127]
|
||||
"F17", // [128]
|
||||
"F18", // [129]
|
||||
"F19", // [130]
|
||||
"F20", // [131]
|
||||
"F21", // [132]
|
||||
"F22", // [133]
|
||||
"F23", // [134]
|
||||
"F24", // [135]
|
||||
"", // [136]
|
||||
"", // [137]
|
||||
"", // [138]
|
||||
"", // [139]
|
||||
"", // [140]
|
||||
"", // [141]
|
||||
"", // [142]
|
||||
"", // [143]
|
||||
"NUM_LOCK", // [144]
|
||||
"SCROLL_LOCK", // [145]
|
||||
"WIN_OEM_FJ_JISHO", // [146]
|
||||
"WIN_OEM_FJ_MASSHOU", // [147]
|
||||
"WIN_OEM_FJ_TOUROKU", // [148]
|
||||
"WIN_OEM_FJ_LOYA", // [149]
|
||||
"WIN_OEM_FJ_ROYA", // [150]
|
||||
"", // [151]
|
||||
"", // [152]
|
||||
"", // [153]
|
||||
"", // [154]
|
||||
"", // [155]
|
||||
"", // [156]
|
||||
"", // [157]
|
||||
"", // [158]
|
||||
"", // [159]
|
||||
"CIRCUMFLEX", // [160]
|
||||
"EXCLAMATION", // [161]
|
||||
"DOUBLE_QUOTE", // [162]
|
||||
"HASH", // [163]
|
||||
"DOLLAR", // [164]
|
||||
"PERCENT", // [165]
|
||||
"AMPERSAND", // [166]
|
||||
"UNDERSCORE", // [167]
|
||||
"OPEN_PAREN", // [168]
|
||||
"CLOSE_PAREN", // [169]
|
||||
"ASTERISK", // [170]
|
||||
"PLUS", // [171]
|
||||
"PIPE", // [172]
|
||||
"HYPHEN_MINUS", // [173]
|
||||
"OPEN_CURLY_BRACKET", // [174]
|
||||
"CLOSE_CURLY_BRACKET", // [175]
|
||||
"TILDE", // [176]
|
||||
"", // [177]
|
||||
"", // [178]
|
||||
"", // [179]
|
||||
"", // [180]
|
||||
"VOLUME_MUTE", // [181]
|
||||
"VOLUME_DOWN", // [182]
|
||||
"VOLUME_UP", // [183]
|
||||
"", // [184]
|
||||
"", // [185]
|
||||
"SEMICOLON", // [186]
|
||||
"EQUALS", // [187]
|
||||
"COMMA", // [188]
|
||||
"MINUS", // [189]
|
||||
"PERIOD", // [190]
|
||||
"SLASH", // [191]
|
||||
"BACK_QUOTE", // [192]
|
||||
"", // [193]
|
||||
"", // [194]
|
||||
"", // [195]
|
||||
"", // [196]
|
||||
"", // [197]
|
||||
"", // [198]
|
||||
"", // [199]
|
||||
"", // [200]
|
||||
"", // [201]
|
||||
"", // [202]
|
||||
"", // [203]
|
||||
"", // [204]
|
||||
"", // [205]
|
||||
"", // [206]
|
||||
"", // [207]
|
||||
"", // [208]
|
||||
"", // [209]
|
||||
"", // [210]
|
||||
"", // [211]
|
||||
"", // [212]
|
||||
"", // [213]
|
||||
"", // [214]
|
||||
"", // [215]
|
||||
"", // [216]
|
||||
"", // [217]
|
||||
"", // [218]
|
||||
"OPEN_BRACKET", // [219]
|
||||
"BACK_SLASH", // [220]
|
||||
"CLOSE_BRACKET", // [221]
|
||||
"QUOTE", // [222]
|
||||
"", // [223]
|
||||
"META", // [224]
|
||||
"ALTGR", // [225]
|
||||
"", // [226]
|
||||
"WIN_ICO_HELP", // [227]
|
||||
"WIN_ICO_00", // [228]
|
||||
"", // [229]
|
||||
"WIN_ICO_CLEAR", // [230]
|
||||
"", // [231]
|
||||
"", // [232]
|
||||
"WIN_OEM_RESET", // [233]
|
||||
"WIN_OEM_JUMP", // [234]
|
||||
"WIN_OEM_PA1", // [235]
|
||||
"WIN_OEM_PA2", // [236]
|
||||
"WIN_OEM_PA3", // [237]
|
||||
"WIN_OEM_WSCTRL", // [238]
|
||||
"WIN_OEM_CUSEL", // [239]
|
||||
"WIN_OEM_ATTN", // [240]
|
||||
"WIN_OEM_FINISH", // [241]
|
||||
"WIN_OEM_COPY", // [242]
|
||||
"WIN_OEM_AUTO", // [243]
|
||||
"WIN_OEM_ENLW", // [244]
|
||||
"WIN_OEM_BACKTAB", // [245]
|
||||
"ATTN", // [246]
|
||||
"CRSEL", // [247]
|
||||
"EXSEL", // [248]
|
||||
"EREOF", // [249]
|
||||
"PLAY", // [250]
|
||||
"ZOOM", // [251]
|
||||
"", // [252]
|
||||
"PA1", // [253]
|
||||
"WIN_OEM_CLEAR", // [254]
|
||||
"" // [255]
|
||||
];
|
||||
|
||||
export default keyboardNameMap;
|
||||
|
|
@ -1,419 +0,0 @@
|
|||
<template>
|
||||
<div
|
||||
class="message"
|
||||
:class="`${message.isChild}ChildMessage`"
|
||||
:id="`${message.modifier}Message`"
|
||||
:key="message.id"
|
||||
>
|
||||
|
||||
<!-- Three types of messages: self (sf), friend (fr), and crimata (ai) -->
|
||||
<!-- !denoted by the modifier attribute -->
|
||||
|
||||
<!-- Shows when a new session is created with Crimata. -->
|
||||
<div
|
||||
v-if="message.context === 'Crimata'"
|
||||
class="divider"
|
||||
>
|
||||
<div>new session</div>
|
||||
</div>
|
||||
|
||||
<!-- Includes bubble and context. -->
|
||||
<div
|
||||
:id="`${message.modifier}MessageBox`"
|
||||
class="messageBox"
|
||||
>
|
||||
|
||||
<!-- message bubble -->
|
||||
<div
|
||||
class="bubble"
|
||||
:class="`${message.modifier}-${message.isChild}ChildBubble`"
|
||||
:id="`${message.modifier}Bubble`"
|
||||
>
|
||||
|
||||
<!-- Notification dot for new messages. -->
|
||||
<div v-if="notify === true"
|
||||
class="notify"
|
||||
/>
|
||||
|
||||
<!-- Show loader when audio is being transcribed. -->
|
||||
<div
|
||||
v-if="message.content.text === ''"
|
||||
class="contentLoader"
|
||||
>
|
||||
<div class="contentLoaderDot"></div>
|
||||
<div class="contentLoaderDot"></div>
|
||||
<div class="contentLoaderDot"></div>
|
||||
</div>
|
||||
|
||||
<!-- Show question mark if transcription comes back as unknown. -->
|
||||
<div
|
||||
v-else-if="message.content.text === false"
|
||||
class="questionMark"
|
||||
>
|
||||
?
|
||||
</div>
|
||||
|
||||
<!-- Else, show text. -->
|
||||
<div v-else>
|
||||
{{ message.content.text }}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- message context -->
|
||||
<span
|
||||
v-if="message.isChild === 'last' || message.isChild === 'none'"
|
||||
class="context"
|
||||
>
|
||||
|
||||
<!-- Render Crimata icon or friend's initials. -->
|
||||
<div v-if="message.modifier == 'ai'" class="photo">
|
||||
<img src="@/assets/crimata.svg"/>
|
||||
</div>
|
||||
|
||||
<!-- Render Crimata icon or friend's initials. -->
|
||||
<div v-if="message.modifier == 'fr'" class="photo"
|
||||
:class="{ playing: isPlaying }"
|
||||
>
|
||||
{{ message.context[0] }}
|
||||
</div>
|
||||
|
||||
{{ message.context }}
|
||||
|
||||
</span>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang='ts'>
|
||||
|
||||
import { defineComponent, ref, onMounted } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: "MessageItem",
|
||||
|
||||
props: ["message"],
|
||||
|
||||
setup(props) {
|
||||
|
||||
let addListener = false;
|
||||
|
||||
const notify = ref(false)
|
||||
const isPlaying = ref(false);
|
||||
|
||||
if (!props.message.seen) {
|
||||
console.log("MSG:Checking...")
|
||||
|
||||
// newMessages always render with blue dot.
|
||||
if (props.message.newMessage) {
|
||||
console.log("MSG:New message, notifying.")
|
||||
props.message.seen = true;
|
||||
notify.value = true;
|
||||
}
|
||||
|
||||
else {
|
||||
|
||||
// Do nothing if window is visible on setup.
|
||||
if (document.visibilityState === "visible") {
|
||||
props.message.seen = true;
|
||||
}
|
||||
|
||||
// We must listen for window visible event.
|
||||
else {
|
||||
console.log("MSG:Unseen, adding listener.")
|
||||
addListener = true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Callback for window visibilityState.
|
||||
const onVisibilityChange = (_event: any) => {
|
||||
if (document.visibilityState === "visible") {
|
||||
console.log("MSG:Window visible, notifying.")
|
||||
|
||||
props.message.seen = true;
|
||||
notify.value = true; // triggers blue dot anim.
|
||||
|
||||
// Once complete we can remove.
|
||||
document.removeEventListener("visibilitychange", onVisibilityChange)
|
||||
}
|
||||
}
|
||||
|
||||
const onStopPlayback = (_event: any) => {
|
||||
window.ipcRenderer.removeAllListeners("stop-playback-anim");
|
||||
isPlaying.value = false
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
// Listen for window focus event.
|
||||
if (addListener) {
|
||||
document.addEventListener("visibilitychange", onVisibilityChange)
|
||||
}
|
||||
|
||||
// Turn on audio playback animation if audio.
|
||||
if (props.message.content.audio === true) {
|
||||
window.ipcRenderer.on("stop-playback-anim", onStopPlayback);
|
||||
isPlaying.value = true
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
return {
|
||||
notify,
|
||||
isPlaying
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
</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;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
@ -1,114 +0,0 @@
|
|||
<template>
|
||||
<InputItem :initials="profile.initials"/>
|
||||
<Settings />
|
||||
|
||||
<div id="recIcon" />
|
||||
|
||||
<!-- List of message bubbles. -->
|
||||
<div id="messenger">
|
||||
<Message
|
||||
v-for="message in messages"
|
||||
:message="message[1]"
|
||||
:key="message[0]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, onMounted, onUnmounted } from "vue";
|
||||
import Message from "@/components/message.vue";
|
||||
import InputItem from "@/components/inputItem.vue";
|
||||
import Settings from "@/components/settings.vue";
|
||||
import useMitt from "@/modules/mitt";
|
||||
import useMessages from "@/modules/messages";
|
||||
|
||||
export default defineComponent({
|
||||
name: "Messenger",
|
||||
|
||||
props: ["profile", "newMessages"],
|
||||
|
||||
components: {
|
||||
Message,
|
||||
InputItem,
|
||||
Settings
|
||||
},
|
||||
|
||||
setup(props) {
|
||||
|
||||
// Listen for self-messages from inputItem.
|
||||
const { emitter } = useMitt();
|
||||
|
||||
// Handle messages in view.
|
||||
const { messages, prepMessageView, updateMessageView } = useMessages();
|
||||
|
||||
onMounted(() => {
|
||||
const crimataId = props.profile.crimataId;
|
||||
console.log(`MSGR:Initializing messenger for ${crimataId}.`)
|
||||
|
||||
// Load the message history.
|
||||
if (crimataId === window.localStorage.getItem("last_usr")) {
|
||||
prepMessageView(props.newMessages);
|
||||
} else {
|
||||
messages.value.clear();
|
||||
}
|
||||
|
||||
// New content listeners.
|
||||
emitter.on('self-message', (message) => updateMessageView(message));
|
||||
window.ipcRenderer.on("render-message", (_e: any, payload: any) => {
|
||||
updateMessageView(payload.message);
|
||||
});
|
||||
|
||||
// Save the usr for next time.
|
||||
window.localStorage.setItem("last_usr", crimataId);
|
||||
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.ipcRenderer.removeAllListeners("render-message");
|
||||
emitter.all.clear();
|
||||
});
|
||||
|
||||
return {
|
||||
messages
|
||||
};
|
||||
|
||||
},
|
||||
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
#messenger {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#messenger::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#recIcon {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
|
||||
opacity: 0;
|
||||
transform: scale(0.0);
|
||||
|
||||
margin-top: 24px;
|
||||
margin-right: 66px;
|
||||
|
||||
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background-color: #FF3B3B;
|
||||
|
||||
z-index: 2;
|
||||
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
<template>
|
||||
|
||||
<input
|
||||
id="textInput"
|
||||
type="text"
|
||||
/>
|
||||
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "TextInput",
|
||||
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
#textInput {
|
||||
position: absolute;
|
||||
|
||||
opacity: 0;
|
||||
|
||||
min-width: 150px;
|
||||
height: 16px;
|
||||
|
||||
border-radius: 18px;
|
||||
|
||||
padding: 10px;
|
||||
margin-right: 10px;
|
||||
margin-left: 10px;
|
||||
|
||||
outline: none;
|
||||
border: none;
|
||||
pointer-events: none;
|
||||
|
||||
background-color: white;
|
||||
|
||||
z-index: -1;
|
||||
|
||||
box-shadow: 0px 0px 10px 0px rgba(0,0,0,0.15);
|
||||
|
||||
transform: translateX(50px) scale(0.3);
|
||||
|
||||
}
|
||||
|
||||
</style>
|
||||
187
src/composables/audio.ts
Normal file
187
src/composables/audio.ts
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
/* eslint @typescript-eslint/no-var-requires: "off" */
|
||||
|
||||
"use strict";
|
||||
|
||||
// where the audio goes
|
||||
let buffer: ArrayBuffer[] = [];
|
||||
|
||||
// place audio data in buffer
|
||||
export function collect (chunk: ArrayBuffer) {
|
||||
buffer.push(chunk);
|
||||
}
|
||||
|
||||
// return audio and clear buffer
|
||||
export function flush () {
|
||||
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;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
31
src/composables/autoUpdate.ts
Normal file
31
src/composables/autoUpdate.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
const { autoUpdater } = require('electron-updater');
|
||||
|
||||
let win: boolean;
|
||||
|
||||
// Listen for window creation.
|
||||
backgroundMitt.on('window-active', (state: boolean) => {
|
||||
win = state;
|
||||
});
|
||||
|
||||
// Auto updating.
|
||||
autoUpdater.requestHeaders = { 'PRIVATE-TOKEN': 'mvvgWYwWnot4bisiQMh_' }
|
||||
|
||||
autoUpdater.on('update-available', (info: any) => {
|
||||
console.log(`Update available: ${info.version}`)
|
||||
})
|
||||
|
||||
autoUpdater.on('update-downloaded', (info: any) => {
|
||||
|
||||
const updateDialog = {
|
||||
type: 'info',
|
||||
buttons: ['Restart', 'Later'],
|
||||
title: 'Application Update',
|
||||
message: info.version,
|
||||
detail: 'A new version has been downloaded. Restart the application to apply the updates.'
|
||||
}
|
||||
|
||||
dialog.showMessageBox(updateDialog).then((returnValue) => {
|
||||
if (returnValue.response === 0) autoUpdater.quitAndInstall()
|
||||
})
|
||||
|
||||
})
|
||||
|
|
@ -1,13 +1,15 @@
|
|||
/* eslint-disable */
|
||||
|
||||
import { Emitter } from "mitt";
|
||||
|
||||
// Backend emitter
|
||||
|
||||
type Mitt = Emitter;
|
||||
|
||||
const EventEmitter = require('events');
|
||||
|
||||
class BackgroundMitt extends EventEmitter { }
|
||||
|
||||
export const backgroundMitt = new BackgroundMitt();
|
||||
|
||||
export default function ipcEmit (channel: string, payload: any) {
|
||||
backgroundMitt.emit('ipc-renderer', {
|
||||
endpoint: channel,
|
||||
message: payload
|
||||
});
|
||||
}
|
||||
9
src/composables/json.ts
Normal file
9
src/composables/json.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
export const saveToJson = (fileName: string, data: any) => {
|
||||
|
||||
fs.writeFile(configPath + fileName, JSON.stringify(data), (err) => {
|
||||
if (err) {
|
||||
console.log("Error when saving to json.")
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
73
src/composables/websockets.ts
Normal file
73
src/composables/websockets.ts
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
import WebSocket from 'ws';
|
||||
|
||||
export default function useWebSockets(receiveCallback: (s: string) => void, openCallback?: () => void) {
|
||||
|
||||
let socket: WebSocket | null = null;
|
||||
|
||||
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 onOpen = (_event: WebSocket.OpenEvent) => {
|
||||
console.log("WS:Connected to WS Server!");
|
||||
if (openCallback) openCallback();
|
||||
}
|
||||
|
||||
const onServerMessage = (event: WebSocket.MessageEvent) => {
|
||||
console.log("WS:Message received: ", event.data);
|
||||
receiveCallback(event.data.toString())
|
||||
}
|
||||
|
||||
const onClose = (event: WebSocket.CloseEvent) => {
|
||||
console.log("WS:Socket closed normally.")
|
||||
}
|
||||
|
||||
// Reconnect automatically on error.
|
||||
const onError = (event: WebSocket.ErrorEvent) => {
|
||||
console.log("WS:WebSocket error: ", event.message);
|
||||
console.log("Attempting reconnect in 1s.")
|
||||
setTimeout(createSocket, 1000);
|
||||
}
|
||||
|
||||
const createSocket = (socketUrl: string) => {
|
||||
|
||||
socket = new WebSocket(socketUrl)
|
||||
|
||||
// Add listeners.
|
||||
socket.addEventListener("open", onOpen);
|
||||
socket.addEventListener("message", onServerMessage);
|
||||
socket.addEventListener("close", onClose);
|
||||
socket.addEventListener("error", onError);
|
||||
|
||||
}
|
||||
|
||||
const close = () => {
|
||||
if (socket) {
|
||||
socket.close();
|
||||
socket = null;
|
||||
}
|
||||
}
|
||||
|
||||
const checkConnection = () => {
|
||||
return true;
|
||||
}
|
||||
|
||||
return {
|
||||
createSocket,
|
||||
send,
|
||||
close,
|
||||
checkConnection
|
||||
};
|
||||
|
||||
}
|
||||
46
src/init.ts
Normal file
46
src/init.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
/**
|
||||
* 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";
|
||||
|
||||
require('dotenv').config();
|
||||
|
||||
console.log('Starting Crimata electron app.');
|
||||
|
||||
// Scheme must be registered before the app is ready
|
||||
protocol.registerSchemesAsPrivileged([
|
||||
{ scheme: "app", privileges: { secure: true, standard: true } }
|
||||
]);
|
||||
|
||||
const isDev = require('electron-is-dev');
|
||||
|
||||
/* Start main process on ready */
|
||||
app.on("ready", async () => {
|
||||
await main();
|
||||
});
|
||||
|
||||
// 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();
|
||||
});
|
||||
}
|
||||
|
|
@ -1,11 +1,9 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
import { Profile } from "@/types";
|
||||
import { submit, fetchProfile, logout } from "@/api/account";
|
||||
import { submit, fetchProfile, logout } from "../api/account";
|
||||
import { ipcMain, IpcMainInvokeEvent } from "electron";
|
||||
import { store } from "@/background/store";
|
||||
import { endSession } from "@/background/session";
|
||||
import { store } from "@/composables/store";
|
||||
|
||||
|
||||
const parseAuthRes = (authRes: any) => {
|
||||
|
|
@ -18,7 +16,13 @@ const parseAuthRes = (authRes: any) => {
|
|||
};
|
||||
|
||||
|
||||
const onProfile = async (
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Get user profile from store and try to login with it.
|
||||
*/
|
||||
const onTokenLogin = async (
|
||||
_event: IpcMainInvokeEvent,
|
||||
_payload: null
|
||||
): Promise<Profile | Error> => (
|
||||
|
|
@ -33,12 +37,11 @@ const onProfile = async (
|
|||
// authenticate and fetch profile
|
||||
try {
|
||||
|
||||
const res = await fetchProfile(
|
||||
crimataId,
|
||||
token
|
||||
);
|
||||
|
||||
// attempt login with email token
|
||||
const res = await fetchProfile(crimataId, token);
|
||||
const parsed = parseAuthRes(res);
|
||||
|
||||
// return profile to renderer
|
||||
resolve(parsed.profile);
|
||||
|
||||
} catch(e) {
|
||||
|
|
@ -60,6 +63,8 @@ const onLogin = async (
|
|||
|
||||
if ( account.password && account.email ) {
|
||||
try {
|
||||
|
||||
// attempt login with email password
|
||||
const res = await submit(account.email, account.password);
|
||||
const parsed = parseAuthRes(res);
|
||||
|
||||
|
|
@ -67,6 +72,8 @@ const onLogin = async (
|
|||
store.set('key', parsed.token);
|
||||
store.set('crimataId', parsed.profile.crimataId);
|
||||
|
||||
// init session
|
||||
|
||||
// return profile to renderer
|
||||
resolve(parsed.profile);
|
||||
|
||||
|
|
@ -95,7 +102,7 @@ const onLogout = async (
|
|||
store.delete('crimataId');
|
||||
|
||||
// TODO: kill crimata platform session
|
||||
endSession();
|
||||
// endSession();
|
||||
|
||||
resolve();
|
||||
} catch(e) {
|
||||
34
src/ipc/audio.ts
Normal file
34
src/ipc/audio.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
import { ipcMain, IpcMainInvokeEvent, IpcMainEvent } from "electron";
|
||||
import { collect, flush } from "@/composbales/audio";
|
||||
|
||||
// handle the new audio data
|
||||
const onAudioChunk = (
|
||||
_e: IpcMainEvent,
|
||||
payload: ArrayBuffer
|
||||
) => {
|
||||
console.log('[IPC]: audio-buffer');
|
||||
collect(payload);
|
||||
}
|
||||
|
||||
// Returns recorded audio to frontend and sets record to false.
|
||||
const onGetAudio = async (
|
||||
_event: IpcMainInvokeEvent,
|
||||
_payload: null
|
||||
): Promise<Error | ArrayBuffer[]> => {
|
||||
console.log('[IPC]: stop-recording');
|
||||
return await flush()
|
||||
};
|
||||
|
||||
|
||||
export default function useAudioListeners(): void {
|
||||
|
||||
ipcMain.removeAllListeners("audio-chunk");
|
||||
ipcMain.on("audio-chunk", onAudioChunk);
|
||||
|
||||
ipcMain.removeHandler("get-audio");
|
||||
ipcMain.handle("get-audio", onGetAudio);
|
||||
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
import useAccountListeners from "./account";
|
||||
import useSessionListeners from "./session";
|
||||
import useAudioListeners from "./audio";
|
||||
// import useAudioListeners from "./audio";
|
||||
|
||||
|
||||
export default function useIpc(): void {
|
||||
|
|
@ -12,6 +12,6 @@ export default function useIpc(): void {
|
|||
|
||||
useSessionListeners();
|
||||
|
||||
useAudioListeners();
|
||||
// useAudioListeners();
|
||||
|
||||
}
|
||||
20
src/ipc/session.ts
Normal file
20
src/ipc/session.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
import { ipcMain, IpcMainEvent, IpcMainInvokeEvent } from "electron";
|
||||
import { sendMessage } from '@/session';
|
||||
|
||||
// Handle messages from window/client.
|
||||
function onSendMessage(_event: IpcMainEvent, payload: Message): void {
|
||||
sendMessage(payload);
|
||||
}
|
||||
|
||||
// Login attempt, returns success or not.
|
||||
function onLogin(_event: IpcMainEvent, payload: LoginPayload) => {
|
||||
authenticate(payload.email, payload.password);
|
||||
}
|
||||
|
||||
export default function useSessionListeners(): void {
|
||||
ipcMain.removeAllListeners("client-message");
|
||||
ipcMain.on("client-message", onSendMessage);
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
|
||||
import { useIpc } from "@/modules/ipc";
|
||||
import { Profile } from "@/types";
|
||||
|
||||
const { invoke } = useIpc();
|
||||
|
||||
interface LoginPayload {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
|
||||
export const invokeProfile = async (): Promise<Profile | Error> => (
|
||||
await invoke('user-profile', null)
|
||||
);
|
||||
|
||||
|
||||
export const invokeLogin = async (
|
||||
payload: LoginPayload
|
||||
): Promise<Profile | Error> => (
|
||||
await invoke('user-login', JSON.stringify(payload))
|
||||
);
|
||||
|
||||
|
||||
export const invokeLogout = async (): Promise<void> => (
|
||||
await invoke("user-logout", null)
|
||||
);
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
|
||||
import { useIpc } from "@/modules/ipc";
|
||||
|
||||
|
||||
const { post, invoke } = useIpc();
|
||||
|
||||
|
||||
export const postStartRecord = (): void => (
|
||||
post("start-recording", null)
|
||||
);
|
||||
|
||||
|
||||
export const invokeStopRecord = async (): Promise<string | Error> => (
|
||||
await invoke("stop-recording", null)
|
||||
);
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
|
||||
import { useIpc } from "@/modules/ipc";
|
||||
|
||||
import { ClientMessage } from "@/types";
|
||||
|
||||
const { post } = useIpc();
|
||||
|
||||
|
||||
export const postMount = (): void => (
|
||||
post("app-mounted", null)
|
||||
);
|
||||
|
||||
|
||||
export const postInitSession = (cid: string): void => (
|
||||
post("init-session", cid)
|
||||
);
|
||||
|
||||
|
||||
export const postMessage = (payload: ClientMessage): void => (
|
||||
post('client-message', payload)
|
||||
);
|
||||
|
||||
|
||||
76
src/main.ts
76
src/main.ts
|
|
@ -1,16 +1,72 @@
|
|||
// src/main.ts
|
||||
/**
|
||||
* Where the background logic really begins, gets called by app.onReady().
|
||||
*
|
||||
* Handles authentication. If profile is set, we launch a session, which consis
|
||||
* of opening a connection with the platform, initializing the audio streams.
|
||||
*
|
||||
* The session is primarily an interface between the frontend and the platform,
|
||||
* relaying messages from one to the other.
|
||||
*
|
||||
*/
|
||||
|
||||
import App from "./App.vue";
|
||||
import { fetchAccount, submit } from "@/api/account";
|
||||
import { launchSession, endSession } from "@/session";
|
||||
import useIpc from "@/ipc/index";
|
||||
import store from "@/composables/store";
|
||||
|
||||
import mitt from "mitt";
|
||||
import { createApp } from "vue";
|
||||
require('dotenv').config()
|
||||
/* user profile, signals whether user is logged in */
|
||||
let auth: Profile | null = null;
|
||||
|
||||
/* authenticate the user */
|
||||
export async function authenticate(email: string, password: string) {
|
||||
|
||||
// Handle events.
|
||||
const emitter = mitt();
|
||||
/* attempt normal login */
|
||||
try {
|
||||
auth = await submit(email, password);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
|
||||
const app = createApp(App)
|
||||
/* launch if profile */
|
||||
if (auth) {
|
||||
launchSession(auth);
|
||||
}
|
||||
|
||||
app.provide("mitt", emitter)
|
||||
app.mount("#app");
|
||||
}
|
||||
|
||||
/* logout the user, end the session */
|
||||
export function deauthenticate() {
|
||||
|
||||
/* set profile back to null */
|
||||
auth = null;
|
||||
|
||||
/* terminate the session */
|
||||
endSession();
|
||||
|
||||
}
|
||||
|
||||
export default async function main() {
|
||||
|
||||
/* launch browser window */
|
||||
// await createWindow();
|
||||
|
||||
/* attempt key-based authentication with business api */
|
||||
const token = store.get('key', null);
|
||||
const crimataId = store.get('crimataId', null);
|
||||
|
||||
try {
|
||||
const res = await fetchAccount(crimataId, token);
|
||||
auth = parseAuthRes(res);
|
||||
} catch (e) {
|
||||
console.log('[MAIN]', e);
|
||||
}
|
||||
|
||||
/* connect to Crimata, or listen for manual login req */
|
||||
if (auth) {
|
||||
launchSession(auth);
|
||||
}
|
||||
|
||||
/* initiate controls for frontend to use when needed */
|
||||
useIpc();
|
||||
|
||||
}
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
import {
|
||||
RenderMessage,
|
||||
ClientMessage,
|
||||
ClientRequest,
|
||||
AuthRequest,
|
||||
LogoutRequest
|
||||
} from "@/types";
|
||||
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
function getTimeStamp(): number {
|
||||
const currentdate = new Date();
|
||||
return currentdate.getTime();
|
||||
}
|
||||
|
||||
// Create a RenderMessage object.
|
||||
export const renderMessage = (text: boolean | string, audio: boolean | string, context: string, modifier: string): RenderMessage => (
|
||||
{
|
||||
content: {
|
||||
text: text,
|
||||
audio: audio
|
||||
},
|
||||
context: context,
|
||||
modifier: modifier,
|
||||
time: getTimeStamp(),
|
||||
uid: uuidv4(),
|
||||
isChild: "none",
|
||||
seen: false,
|
||||
newMessage: false
|
||||
}
|
||||
)
|
||||
|
||||
export const clientMessage = (text: string, audio: string | boolean, uid: string): ClientMessage => (
|
||||
{
|
||||
audio,
|
||||
text,
|
||||
uid
|
||||
}
|
||||
)
|
||||
|
||||
export const clientRequest = (intent: string, params: object, epic: string | boolean): ClientRequest => (
|
||||
{
|
||||
intent: intent,
|
||||
params: params,
|
||||
epic: epic,
|
||||
confidence: 1.0
|
||||
}
|
||||
)
|
||||
|
||||
export const authRequest = (key: boolean | string, usr: boolean | string, pwd: boolean | string): AuthRequest => (
|
||||
{
|
||||
key,
|
||||
usr,
|
||||
pwd
|
||||
}
|
||||
)
|
||||
|
||||
export const logoutRequest = (): LogoutRequest => (
|
||||
{
|
||||
logout: true
|
||||
}
|
||||
)
|
||||
|
|
@ -1,125 +0,0 @@
|
|||
import { ref } from 'vue';
|
||||
import useScroll from "@/modules/scroll";
|
||||
import { RenderMessage, Annotation } from "@/types";
|
||||
|
||||
const messages = ref(new Map());
|
||||
|
||||
const addMessage = (message: RenderMessage) => {
|
||||
messages.value.set(message.uid, message)
|
||||
}
|
||||
|
||||
const updateMessage = (annotation: Annotation) => {
|
||||
const message = messages.value.get(annotation.uid)
|
||||
message.context = annotation.context
|
||||
message.content.text = annotation.text
|
||||
}
|
||||
|
||||
const loadSavedMessages = () => {
|
||||
const rawData = window.localStorage.getItem("crimata_messages");
|
||||
if (rawData) {
|
||||
const messageData = JSON.parse(rawData)
|
||||
messages.value = new Map(Object.entries(messageData));
|
||||
}
|
||||
}
|
||||
|
||||
const saveMessages = () => {
|
||||
const messageData = Object.fromEntries(messages.value);
|
||||
window.localStorage.setItem("crimata_messages", JSON.stringify(messageData));
|
||||
}
|
||||
|
||||
const isSimmilar = (messageA: RenderMessage, messageB: RenderMessage) => {
|
||||
if ((Math.abs(messageA.time - messageB.time) < 20000) && (messageA.modifier == messageB.modifier) && (messageA.context == messageB.context)) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const updateGrouping = () => {
|
||||
console.log("updating grouping")
|
||||
const refs = Array.from(messages.value.keys())
|
||||
|
||||
// Get the last three messages.
|
||||
const first = messages.value.get(refs[refs.length - 1])
|
||||
const second = messages.value.get(refs[refs.length - 2])
|
||||
const third = messages.value.get(refs[refs.length - 3])
|
||||
|
||||
// If messages are simmilar, update the classes.
|
||||
if ((first) && (second)) {
|
||||
if (isSimmilar(first, second)) {
|
||||
first.isChild = "last" // i.e. last in group.
|
||||
second.isChild = "first"
|
||||
|
||||
if (third) {
|
||||
if ((third.isChild == "first") || (third.isChild == "middle")) {
|
||||
second.isChild = "middle"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default function useMessages() {
|
||||
|
||||
// Scroll controller.
|
||||
const { setScroll, updateScrollRef, adjustScroll } = useScroll("messenger");
|
||||
|
||||
// Main function for updating the message view.
|
||||
const updateMessageView = (message: RenderMessage | Annotation) => {
|
||||
|
||||
// Step 1: See if user is scrolled down.
|
||||
updateScrollRef()
|
||||
|
||||
// Step 2: Add the new content to the view.
|
||||
if ("content" in message) {
|
||||
addMessage(message)
|
||||
} else {
|
||||
updateMessage(message)
|
||||
}
|
||||
|
||||
// Step 3: Pop off oldest message (if > 200).
|
||||
if (messages.value.size >= 200) {
|
||||
const oldest = Array.from(messages.value.keys()).shift();
|
||||
messages.value.delete(oldest);
|
||||
}
|
||||
|
||||
// Step 4: Update grouping.
|
||||
updateGrouping()
|
||||
|
||||
// Setp 5: Scroll the view (if scrolled down).
|
||||
setTimeout(adjustScroll, 20);
|
||||
|
||||
// Step 6: Save the view data.
|
||||
saveMessages()
|
||||
|
||||
}
|
||||
|
||||
// Seed message view with message history.
|
||||
const prepMessageView = (newMessages: RenderMessage[]) => {
|
||||
console.log("MSGR:Prepping messenger view.")
|
||||
|
||||
// Load and render saved messages and immediately scroll to bottom.
|
||||
loadSavedMessages()
|
||||
setTimeout(setScroll.bind(false), 10);
|
||||
|
||||
// Render new messages, then wait 1s to scroll.
|
||||
if (newMessages.length) {
|
||||
|
||||
console.log("MSGR:Adding new messages")
|
||||
|
||||
newMessages.forEach(message => {
|
||||
message.newMessage = true;
|
||||
addMessage(message)
|
||||
})
|
||||
|
||||
setTimeout(setScroll.bind(true), 1000);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
messages,
|
||||
prepMessageView,
|
||||
updateMessageView
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
import { inject } from "vue";
|
||||
import { Emitter } from "mitt";
|
||||
|
||||
// Frontend emitter
|
||||
|
||||
type Mitt = Emitter;
|
||||
|
||||
let emitter: Mitt;
|
||||
|
||||
export default function useMitt() {
|
||||
|
||||
const emitterInject: Mitt | undefined = inject("mitt");
|
||||
if (emitterInject) {
|
||||
emitter = emitterInject;
|
||||
}
|
||||
return {
|
||||
emitter
|
||||
}
|
||||
}
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
|
||||
|
||||
export default function useScroll(element: string) {
|
||||
|
||||
let isScrolledToBottom: boolean;
|
||||
|
||||
// Set the initial scroll position.
|
||||
const setScroll = (smooth: boolean) => {
|
||||
const view = document.getElementById(element)
|
||||
|
||||
if (view) {
|
||||
view.scrollTo({
|
||||
top: view.scrollHeight - view.clientHeight,
|
||||
behavior: (smooth) ? 'smooth' : 'auto'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Update isScrolledToBottom
|
||||
const updateScrollRef = () => {
|
||||
const view = document.getElementById(element)
|
||||
|
||||
if (view) {
|
||||
isScrolledToBottom = view.scrollHeight - view.clientHeight <= view.scrollTop + 1
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Adjust scroll after we add content to the messenger.
|
||||
const adjustScroll = () => {
|
||||
const view = document.getElementById(element)
|
||||
|
||||
if (view) {
|
||||
if (isScrolledToBottom) {
|
||||
view.scrollTo({
|
||||
top: view.scrollHeight - view.clientHeight,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return {
|
||||
updateScrollRef,
|
||||
adjustScroll,
|
||||
setScroll,
|
||||
}
|
||||
|
||||
}
|
||||
86
src/render/App.vue
Normal file
86
src/render/App.vue
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
<template>
|
||||
<!-- Only render when profile has been set -->
|
||||
<main id="app" v-if="typeof profile !== 'undefined'">
|
||||
|
||||
<Header />
|
||||
|
||||
<!-- Main Components -->
|
||||
<Messenger
|
||||
v-if="profile"
|
||||
:profile="profile"
|
||||
/>
|
||||
<Login v-else />
|
||||
|
||||
</main>
|
||||
|
||||
<!-- Show spash screen if ready is false -->
|
||||
<Splash v-else />
|
||||
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
import { defineComponent, onMounted, onUnmounted, Ref } from "vue";
|
||||
import { invokeProfile } from "@/ipc/account";
|
||||
|
||||
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";
|
||||
|
||||
export default defineComponent({
|
||||
|
||||
components: {
|
||||
Splash,
|
||||
Messenger,
|
||||
Login,
|
||||
Header
|
||||
},
|
||||
|
||||
setup() {
|
||||
|
||||
const state: Ref;
|
||||
|
||||
onMounted(async () => {
|
||||
console.log("[APP]:mounted.");
|
||||
|
||||
/* listen for auth related messages */
|
||||
window.addEventListener("update-state", (event: any) => {
|
||||
state.value = event.data;
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.ipcRenderer.removeAllListeners("update-state");
|
||||
});
|
||||
|
||||
return {
|
||||
profile
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
// Background color set in window.ts
|
||||
}
|
||||
|
||||
#app {
|
||||
font-family: "SF Pro Text";
|
||||
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
</style>
|
||||
330
src/render/components/bubble.vue
Normal file
330
src/render/components/bubble.vue
Normal file
|
|
@ -0,0 +1,330 @@
|
|||
<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: "MessageItem",
|
||||
|
||||
props: ["text", "context", "type", "position"],
|
||||
|
||||
setup() {
|
||||
|
||||
/* initialize seen state */
|
||||
const seen = ref(() => {
|
||||
if (document.visibilityState === "visible") {
|
||||
return true;
|
||||
} else return false;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
if (!seen)
|
||||
document.addEventListener("visibilitychange", () => seen = 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>
|
||||
|
||||
|
||||
0
src/render/components/controllers/bubble.control.ts
Normal file
0
src/render/components/controllers/bubble.control.ts
Normal file
102
src/render/components/controllers/helpers.ts
Normal file
102
src/render/components/controllers/helpers.ts
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
import { ref } from "vue";
|
||||
import anime from "animejs";
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
|
||||
export function animateTextInput () {
|
||||
|
||||
const side = ref("right");
|
||||
|
||||
function show () {
|
||||
const t1 = (side.value === "right") ? 50 : -70;
|
||||
const t2 = (side.value === "right") ? 110 : -130;
|
||||
|
||||
anime({
|
||||
targets: '#textInput',
|
||||
opacity: [0, 1],
|
||||
translateX: [t1, t2],
|
||||
scale: [0.3, 1],
|
||||
duration: 500,
|
||||
easing: 'easeOutExpo',
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
function hide () {
|
||||
const t = (side.value === "right") ? 50 : -80;
|
||||
|
||||
anime({
|
||||
targets: '#textInput',
|
||||
opacity: [1, 0],
|
||||
translateX: t,
|
||||
scale: 0.3,
|
||||
duration: 500,
|
||||
easing: 'easeOutExpo',
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
function switchSide () {
|
||||
const t = (side.value === "right") ? -130 : 110;
|
||||
|
||||
anime({
|
||||
targets: '#textInput',
|
||||
translateX: t,
|
||||
duration: 500,
|
||||
easing: 'easeOutExpo',
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
return {
|
||||
side,
|
||||
show,
|
||||
hide,
|
||||
switchSide
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
export function animateAudioInput () {
|
||||
|
||||
function show () {
|
||||
anime({
|
||||
targets: '#recIcon',
|
||||
opacity: [0, 0.75],
|
||||
scale: [0.0, 1],
|
||||
duration: 250,
|
||||
easing: 'linear',
|
||||
})
|
||||
}
|
||||
|
||||
function hide () {
|
||||
anime({
|
||||
targets: '#recIcon',
|
||||
opacity: [0.75, 0],
|
||||
scale: [1, 0],
|
||||
duration: 250,
|
||||
easing: 'linear',
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
show,
|
||||
hide
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
|
||||
export function newMessage ({
|
||||
text=false,
|
||||
audio=false,
|
||||
context=false,
|
||||
uid=uuidv4()
|
||||
}): Message {
|
||||
return {
|
||||
text: text,
|
||||
audio: audio,
|
||||
context: context,
|
||||
uid: uid
|
||||
};
|
||||
}
|
||||
79
src/render/components/controllers/inputItem.control.audio.ts
Normal file
79
src/render/components/controllers/inputItem.control.audio.ts
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
import anime from "animejs";
|
||||
import { useIpc } from '@/modules/ipc';
|
||||
import { onMounted, onUnmounted, ref, Ref } from "vue";
|
||||
import { postMessage } from "@/ipc/session";
|
||||
import { newMessage, animateAudioInput } from "./helpers";
|
||||
import { invokeStopRecord, postAudioChunk } from "@/ipc/audio";
|
||||
|
||||
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) => {
|
||||
invokeStopRecord().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,72 +1,20 @@
|
|||
import anime from "animejs";
|
||||
import useMitt from "@/modules/mitt";
|
||||
import { useIpc } from '@/modules/ipc';
|
||||
import { Ref, ref, watch, onMounted, onUnmounted } from "vue";
|
||||
import keyboardNameMap from "../keyBoardMaps/keyboardNameMap";
|
||||
import { clientMessage, renderMessage } from '@/modules/message';
|
||||
import { postMessage } from "@/ipcRend/session";
|
||||
|
||||
|
||||
//---Animations-----------------------------------------------
|
||||
|
||||
let side = "right"; // Side of parent we are on.
|
||||
|
||||
function showTextInput () {
|
||||
const t1 = (side === "right") ? 50 : -70;
|
||||
const t2 = (side === "right") ? 110 : -130;
|
||||
|
||||
anime({
|
||||
targets: '#textInput',
|
||||
opacity: [0, 1],
|
||||
translateX: [t1, t2],
|
||||
scale: [0.3, 1],
|
||||
duration: 500,
|
||||
easing: 'easeOutExpo',
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
function hideTextInput() {
|
||||
const t = (side === "right") ? 50 : -80;
|
||||
|
||||
anime({
|
||||
targets: '#textInput',
|
||||
opacity: [1, 0],
|
||||
translateX: t,
|
||||
scale: 0.3,
|
||||
duration: 500,
|
||||
easing: 'easeOutExpo',
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
function switchSide(currentSide: string) {
|
||||
const t = (currentSide === "right") ? -130 : 110;
|
||||
|
||||
anime({
|
||||
targets: '#textInput',
|
||||
translateX: t,
|
||||
duration: 500,
|
||||
easing: 'easeOutExpo',
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
//------------------------------------------------------------
|
||||
import { postMessage } from "@/ipc/session";
|
||||
import { newMessage, animateTextInput } from "./helpers";
|
||||
|
||||
|
||||
export default function useTextInputController(elementX: Ref) {
|
||||
|
||||
let textInput: HTMLInputElement | null;
|
||||
|
||||
const { post } = useIpc();
|
||||
const { emitter } = useMitt();
|
||||
const { side, show, hide, switchSide } = animateTextInput();
|
||||
|
||||
let firstKey = true;
|
||||
const typing = ref(false);
|
||||
|
||||
// Prep inputItem for typing.
|
||||
const prepInput = () => {
|
||||
showTextInput()
|
||||
show()
|
||||
typing.value = true
|
||||
}
|
||||
|
||||
|
|
@ -78,7 +26,7 @@ export default function useTextInputController(elementX: Ref) {
|
|||
textInput.blur();
|
||||
}
|
||||
|
||||
hideTextInput()
|
||||
hide()
|
||||
firstKey = true;
|
||||
typing.value = false;
|
||||
}
|
||||
|
|
@ -87,28 +35,20 @@ export default function useTextInputController(elementX: Ref) {
|
|||
const sendMessage = () => {
|
||||
if (textInput) {
|
||||
|
||||
// Create the message.
|
||||
const message = renderMessage(
|
||||
textInput.value,
|
||||
false,
|
||||
"",
|
||||
"sf"
|
||||
)
|
||||
|
||||
emitter.emit("self-message", message);
|
||||
|
||||
// Send it to the backend for processing.
|
||||
const clientM = clientMessage(textInput.value, false, message.uid);
|
||||
postMessage(clientM);
|
||||
const message = newMessage({
|
||||
text: textInput.value
|
||||
});
|
||||
|
||||
postMessage(message);
|
||||
|
||||
clearInput()
|
||||
}
|
||||
}
|
||||
|
||||
// Keys that are capable of opening the text input (numbers and letters).
|
||||
const hotKeyRange = keyboardNameMap.slice(47, 91)
|
||||
const isHotKey = (key: string) => {
|
||||
if (hotKeyRange.includes(key)) {
|
||||
const isHotKey = (key: number) => {
|
||||
if (key >= 47 && key <= 91) { // a letter
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
@ -116,7 +56,7 @@ export default function useTextInputController(elementX: Ref) {
|
|||
//---Callbacks-----------------------------------------------
|
||||
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
const key = keyboardNameMap[e.keyCode]
|
||||
const key = e.keyCode;
|
||||
|
||||
if (textInput) {
|
||||
|
||||
|
|
@ -133,18 +73,18 @@ export default function useTextInputController(elementX: Ref) {
|
|||
textInput.focus();
|
||||
|
||||
// Close input when no text or on ESC.
|
||||
if ((textInput.value == "") && (!firstKey) && (key === "BACK_SPACE")) {
|
||||
if ((textInput.value == "") && (!firstKey) && (key === 8)) { // backspace
|
||||
clearInput()
|
||||
return
|
||||
}
|
||||
|
||||
if (key === "ESCAPE") {
|
||||
if (key === 27) { // escape
|
||||
clearInput()
|
||||
return
|
||||
}
|
||||
|
||||
// Close and send on enter.
|
||||
if (key === "ENTER") {
|
||||
if (key === 13) {
|
||||
if (textInput.value) {
|
||||
sendMessage()
|
||||
return
|
||||
|
|
@ -162,17 +102,17 @@ export default function useTextInputController(elementX: Ref) {
|
|||
const winW = window.innerWidth
|
||||
|
||||
// Logic depends on the side we are on.
|
||||
if (side === "right") {
|
||||
if (side.value === "right") {
|
||||
if (winW - elementX < 230) {
|
||||
switchSide(side)
|
||||
side = "left"
|
||||
switchSide()
|
||||
side.value = "left"
|
||||
}
|
||||
}
|
||||
|
||||
else {
|
||||
if (winW - elementX > 230) {
|
||||
switchSide(side)
|
||||
side = "right"
|
||||
switchSide()
|
||||
side.value = "right"
|
||||
}
|
||||
}
|
||||
|
||||
143
src/render/components/controllers/messenger.control.ts
Normal file
143
src/render/components/controllers/messenger.control.ts
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
import { ref } from 'vue';
|
||||
import invokeSavedMessages from "@/render/ipc";
|
||||
import useScroll from "@/render/composables/scroll";
|
||||
|
||||
const messages = ref(new Map());
|
||||
|
||||
function getTimeStamp(): number {
|
||||
const currentdate = new Date();
|
||||
return currentdate.getTime();
|
||||
}
|
||||
|
||||
const newViewMessage = (message: Message): ViewMessage => {
|
||||
return {
|
||||
text: message.text,
|
||||
context: message.context,
|
||||
audio: message.audio,
|
||||
from: message.from,
|
||||
uid: message.uid,
|
||||
time: getTimeStamp(),
|
||||
isChild: "none",
|
||||
seen: false,
|
||||
newMessage: false
|
||||
};
|
||||
}
|
||||
|
||||
const addMessage = (message: Message, newMessage=false) => {
|
||||
const viewMessage = newViewMessage(message);
|
||||
if (newMessage) viewMessage.newMessage = true;
|
||||
messages.value.set(viewMessage.uid, viewMessage);
|
||||
}
|
||||
|
||||
const updateMessage = (message: Message) => {
|
||||
const viewMessage = messages.value.get(message.uid);
|
||||
viewMessage.context = message.context;
|
||||
viewMessage.text = message.text;
|
||||
}
|
||||
|
||||
const loadSavedMessages = async () => {
|
||||
const messageData = await invokeSavedMessages();
|
||||
messages.value = new Map(Object.entries(messageData));
|
||||
}
|
||||
|
||||
const saveMessages = () => {
|
||||
const messageData = Object.fromEntries(messages.value);
|
||||
// must save to json.
|
||||
}
|
||||
|
||||
const pruneMessages = (limit=200) => {
|
||||
if (messages.value.size >= limit) {
|
||||
const oldest = Array.from(messages.value.keys()).shift();
|
||||
messages.value.delete(oldest);
|
||||
}
|
||||
}
|
||||
|
||||
const updateGrouping = () => {
|
||||
|
||||
const isSimmilar = (messageA: ViewMessage, messageB: ViewMessage) => {
|
||||
if ((Math.abs(messageA.time - messageB.time) < 20000) && (messageA.from == messageB.from) && (messageA.context == messageB.context)) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const refs = Array.from(messages.value.keys())
|
||||
|
||||
// Get the last three messages.
|
||||
const first = messages.value.get(refs[refs.length - 1])
|
||||
const second = messages.value.get(refs[refs.length - 2])
|
||||
const third = messages.value.get(refs[refs.length - 3])
|
||||
|
||||
// If messages are simmilar, update the classes.
|
||||
if ((first) && (second)) {
|
||||
if (isSimmilar(first, second)) {
|
||||
first.isChild = "last" // i.e. last in group.
|
||||
second.isChild = "first"
|
||||
|
||||
if (third) {
|
||||
if ((third.isChild == "first") || (third.isChild == "middle")) {
|
||||
second.isChild = "middle"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default function useMessages() {
|
||||
|
||||
const { updateScrollRef, adjustScroll } = useScroll("messenger");
|
||||
|
||||
/* Given new message object, update the view accordingly */
|
||||
const updateMessageView = (newMessages: Message[]) => {
|
||||
|
||||
const bottom = updateScrollRef(); // see if the user is scrolled down
|
||||
|
||||
// take each message and apply view
|
||||
newMessages.forEach((message: Message) => {
|
||||
|
||||
// add or update message depending
|
||||
if (messages.value.has(message.uid)) {
|
||||
updateMessage(message);
|
||||
} else addMessage(message);
|
||||
|
||||
pruneMessages(); // pop off old messages from view
|
||||
updateGrouping(); // group like message together
|
||||
|
||||
if (bottom) adjustScroll(); // only scroll if user was at bottom
|
||||
|
||||
saveMessages();
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
return {
|
||||
messages,
|
||||
updateMessageView,
|
||||
loadSavedMessages
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
// // Seed message view with message history.
|
||||
// const prepMessageView = async (newMessages: Message[]) => {
|
||||
// console.log("MSGR:Prepping messenger view.")
|
||||
|
||||
// // Load and render saved messages and immediately scroll to bottom.
|
||||
// await loadSavedMessages();
|
||||
// setTimeout(setScroll.bind(false), 10);
|
||||
|
||||
// // Render new messages, then wait 1s to scroll.
|
||||
// if (newMessages.length) {
|
||||
|
||||
// console.log("MSGR:Adding new messages")
|
||||
|
||||
// newMessages.forEach(message => {
|
||||
// addMessage(message, true);
|
||||
// })
|
||||
|
||||
// setTimeout(setScroll.bind(true), 1000);
|
||||
|
||||
// }
|
||||
// }
|
||||
81
src/render/components/header.vue
Normal file
81
src/render/components/header.vue
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
<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";
|
||||
|
||||
setup() {
|
||||
return {
|
||||
postNavBarExit,
|
||||
postNavBarMin
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
#titlebar {
|
||||
position: fixed;
|
||||
|
||||
width: 100vw;
|
||||
height: 54px;
|
||||
|
||||
opacity: 0.75;
|
||||
|
||||
background-color: #EBEBEB;
|
||||
|
||||
-webkit-app-region: drag;
|
||||
|
||||
border: none;
|
||||
outline: none;
|
||||
|
||||
z-index: 1;
|
||||
|
||||
}
|
||||
|
||||
#menu {
|
||||
position: fixed;
|
||||
margin-left: 20px;
|
||||
margin-top: 20px;
|
||||
|
||||
z-index: 2;
|
||||
|
||||
}
|
||||
|
||||
.menuButton {
|
||||
border: none;
|
||||
outline:none;
|
||||
min-width: 14px;
|
||||
min-height: 14px;
|
||||
border-radius: 7px;
|
||||
}
|
||||
|
||||
.exitButton {
|
||||
background-color: #FF6157;
|
||||
}
|
||||
|
||||
.exitButton:active {
|
||||
background: #c14645;
|
||||
}
|
||||
|
||||
.minimizeButton {
|
||||
background-color: #FFC12F;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.minimizeButton:active {
|
||||
background-color: #c08e38;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -13,7 +13,10 @@
|
|||
<span v-if="recording" class="pause"></span>
|
||||
|
||||
<!-- Show text input on key-down -->
|
||||
<TextInput />
|
||||
<input
|
||||
id="textInput"
|
||||
type="text"
|
||||
/>
|
||||
|
||||
<!-- Show suggestions menu on click -->
|
||||
<!-- <Menu /> -->
|
||||
|
|
@ -29,9 +32,9 @@ import draggify from "@/modules/draggify";
|
|||
import TextInput from "@/components/textInput.vue";
|
||||
|
||||
import useTextInputController from
|
||||
"@/components/controllers/textCtrl";
|
||||
"@/components/controllers/inputItem.control.audio";
|
||||
import useAudioInputController from
|
||||
"@/components/controllers/audioCtrl";
|
||||
"@/components/controllers/inputItem.control.text";
|
||||
|
||||
export default defineComponent({
|
||||
name: "InputItem",
|
||||
|
|
@ -191,4 +194,32 @@ export default defineComponent({
|
|||
}
|
||||
}
|
||||
|
||||
#textInput {
|
||||
position: absolute;
|
||||
|
||||
opacity: 0;
|
||||
|
||||
min-width: 150px;
|
||||
height: 16px;
|
||||
|
||||
border-radius: 18px;
|
||||
|
||||
padding: 10px;
|
||||
margin-right: 10px;
|
||||
margin-left: 10px;
|
||||
|
||||
outline: none;
|
||||
border: none;
|
||||
pointer-events: none;
|
||||
|
||||
background-color: white;
|
||||
|
||||
z-index: -1;
|
||||
|
||||
box-shadow: 0px 0px 10px 0px rgba(0,0,0,0.15);
|
||||
|
||||
transform: translateX(50px) scale(0.3);
|
||||
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
@ -39,7 +39,6 @@ import { useIpc } from "@/modules/ipc";
|
|||
import { authRequest } from '@/modules/message';
|
||||
import { useProfile } from '@/modules/auth';
|
||||
import { invokeLogin } from "@/ipcRend/account";
|
||||
import { Profile } from "@/types";
|
||||
import { postInitSession } from "@/ipcRend/session";
|
||||
|
||||
export default defineComponent({
|
||||
|
|
@ -47,8 +46,6 @@ export default defineComponent({
|
|||
|
||||
setup() {
|
||||
|
||||
const { profile, setProfile } = useProfile();
|
||||
|
||||
const usr = ref("");
|
||||
const pwd = ref("");
|
||||
|
||||
|
|
@ -56,23 +53,17 @@ export default defineComponent({
|
|||
const submitForm = async () => {
|
||||
|
||||
try {
|
||||
|
||||
const profile = await invokeLogin({
|
||||
email: usr.value,
|
||||
password: pwd.value
|
||||
}) as Profile;
|
||||
});
|
||||
|
||||
setProfile(profile);
|
||||
/* emit event to app.vue */
|
||||
window.postMessage(profile);
|
||||
|
||||
} catch(e) {
|
||||
console.log('Error login in.')
|
||||
} finally{
|
||||
} catch (e) console.log(e);
|
||||
|
||||
if (profile.value.crimataId) {
|
||||
// start session
|
||||
postInitSession(profile.value.crimataId);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
101
src/render/components/messenger.vue
Normal file
101
src/render/components/messenger.vue
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
<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.text"
|
||||
:context="message.context"
|
||||
:key="message[0]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
import { defineComponent, onMounted, onUnmounted } from "vue";
|
||||
import Message from "@/render/components/message.vue";
|
||||
import InputItem from "@/render/components/inputItem.vue";
|
||||
import Settings from "@/render/components/settings.vue";
|
||||
import useMessages from "@/render/composables/messages";
|
||||
|
||||
export default defineComponent({
|
||||
name: "Messenger",
|
||||
|
||||
props: ["state"],
|
||||
|
||||
components: {
|
||||
Message,
|
||||
InputItem,
|
||||
Settings
|
||||
},
|
||||
|
||||
setup(props) {
|
||||
|
||||
// Handle messages in view.
|
||||
const { messages, updateMessageView } = useMessages();
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
/* populate the message view with existing messages */
|
||||
updateMessageView(state.savedMessages, state.newMessages);
|
||||
|
||||
/* wait and listen for new messages to come in */
|
||||
window.ipcRenderer.on("new-message", (_e: any, payload: any) => {
|
||||
updateMessageView(payload.message);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.ipcRenderer.removeAllListeners("new-message");
|
||||
});
|
||||
|
||||
return {
|
||||
messages
|
||||
};
|
||||
|
||||
},
|
||||
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
#messenger {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#messenger::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#recIcon {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
|
||||
opacity: 0;
|
||||
transform: scale(0.0);
|
||||
|
||||
margin-top: 24px;
|
||||
margin-right: 66px;
|
||||
|
||||
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background-color: #FF3B3B;
|
||||
|
||||
z-index: 2;
|
||||
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
@ -1,6 +1,4 @@
|
|||
import { ref } from "vue";
|
||||
import { Profile } from "@/types";
|
||||
|
||||
|
||||
const profile = ref();
|
||||
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
|
||||
export const useIpc = () => {
|
||||
export default function useIpc () {
|
||||
|
||||
const invoke = async (endpoint: string, payload: any) => {
|
||||
try {
|
||||
29
src/render/composables/scroll.ts
Normal file
29
src/render/composables/scroll.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
|
||||
|
||||
export default function useScroll(element: string) {
|
||||
|
||||
let isScrolledToBottom: boolean;
|
||||
const view = document.getElementById(element)
|
||||
|
||||
// Update isScrolledToBottom
|
||||
const updateScrollRef = () => {
|
||||
if (view) isScrolledToBottom = view.scrollHeight - view.clientHeight <= view.scrollTop + 1;
|
||||
return isScrolledToBottom;
|
||||
}
|
||||
|
||||
// Adjust scroll after we add content to the messenger.
|
||||
const adjustScroll = () => {
|
||||
if (view) {
|
||||
view.scrollTo({
|
||||
top: view.scrollHeight - view.clientHeight,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
updateScrollRef,
|
||||
adjustScroll,
|
||||
};
|
||||
|
||||
}
|
||||
52
src/render/ipc.ts
Normal file
52
src/render/ipc.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
|
||||
import useIpc from "@/render/composables/ipc";
|
||||
|
||||
const { post, invoke } = useIpc();
|
||||
|
||||
/**
|
||||
*
|
||||
* Account and auth related endpoints
|
||||
*
|
||||
*/
|
||||
|
||||
export const invokeProfile = async (): Promise<Profile | Error> => (
|
||||
await invoke('user-profile', null)
|
||||
);
|
||||
|
||||
export const invokeLogin = async (
|
||||
payload: LoginPayload
|
||||
): Promise<Profile | Error> => (
|
||||
await invoke('user-login', JSON.stringify(payload))
|
||||
);
|
||||
|
||||
export const invokeLogout = async (): Promise<void> => (
|
||||
await invoke("user-logout", null)
|
||||
);
|
||||
|
||||
/**
|
||||
*
|
||||
* Audio endpoints
|
||||
*
|
||||
*/
|
||||
|
||||
export const postAudioChunk = (chunk: ArrayBuffer): void => (
|
||||
post("audio-chunk", chunk)
|
||||
);
|
||||
|
||||
export const invokeReturnAudio = async (): Promise<ArrayBuffer[] | Error> => (
|
||||
await invoke("get-audio", 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('client-message', payload)
|
||||
);
|
||||
64
src/session.ts
Normal file
64
src/session.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
|
||||
|
||||
|
||||
|
||||
import useAudio from "@/audio";
|
||||
import { loadState, saveState, emitState } from "@/state";
|
||||
|
||||
/* start and stop audio functionality */
|
||||
const { initAudio, closeAudio } = useAudio();
|
||||
|
||||
/**
|
||||
* Controls for interfacing with the platform.
|
||||
* Takes an onMessage callback which we define below.
|
||||
*/
|
||||
const { connect, send, close } = usePlatform((message: Message) => {
|
||||
|
||||
/* add the message to the state */
|
||||
addMessage(message);
|
||||
|
||||
/* push the message to the browser */
|
||||
if (win) emit("new-message", message);
|
||||
|
||||
});
|
||||
|
||||
/* send a message to the platform */
|
||||
export function sendMessage(message: Message) {
|
||||
|
||||
/* add the message to the state */
|
||||
addMessage(message);
|
||||
|
||||
/* push the message to the browser */
|
||||
if (win) emit("new-message", message);
|
||||
|
||||
/* socket send */
|
||||
send(message);
|
||||
|
||||
}
|
||||
|
||||
/* launch a new session (the main process for authenticated users) */
|
||||
export function launchSession(profile: Profile) {
|
||||
|
||||
/* load any previously saved state for that user */
|
||||
loadState(profile);
|
||||
|
||||
/* connect to the platform */
|
||||
connect(profile);
|
||||
|
||||
/* initialize the audio streams */
|
||||
// initAudio();
|
||||
|
||||
/* finally we can push state to browser */
|
||||
if (win) emitState();
|
||||
|
||||
}
|
||||
|
||||
export function endSession() {
|
||||
|
||||
closeAudioStreams();
|
||||
|
||||
closeSocket();
|
||||
|
||||
state.clear();
|
||||
|
||||
}
|
||||
29
src/state.ts
Normal file
29
src/state.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
const Store = require('electron-store');
|
||||
|
||||
/* simple data persistance */
|
||||
const store = new Store;
|
||||
|
||||
/* state of the session (e.g. profile and messages for now) */
|
||||
let state: State | null = null;
|
||||
|
||||
/* load saved state in electron store for given user */
|
||||
export function loadState(profile: Profile) {
|
||||
state = store.get("state", null);
|
||||
}
|
||||
|
||||
/* add a message to state.messages */
|
||||
export function addMessage(message: Message) {
|
||||
if (state) {
|
||||
state.messages.push(message);
|
||||
saveState();
|
||||
}
|
||||
}
|
||||
|
||||
export function saveState() {
|
||||
store.set("state", state);
|
||||
}
|
||||
|
||||
export function emitState() {
|
||||
emit("update-state", state);
|
||||
}
|
||||
|
||||
73
src/types.ts
73
src/types.ts
|
|
@ -1,76 +1,37 @@
|
|||
export interface RenderMessage {
|
||||
content: {
|
||||
text: boolean | string;
|
||||
audio: boolean | string;
|
||||
};
|
||||
context: string;
|
||||
modifier: string;
|
||||
interface Message {
|
||||
text: boolean | string;
|
||||
context: boolean | string;
|
||||
audio: boolean | string;
|
||||
type: 1 | 2 | 3;
|
||||
time: number;
|
||||
uid: string;
|
||||
isChild: string;
|
||||
}
|
||||
|
||||
interface ViewMessage extends Message {
|
||||
child: string;
|
||||
seen: boolean;
|
||||
newMessage: boolean;
|
||||
}
|
||||
|
||||
export interface ClientMessage {
|
||||
text: string;
|
||||
audio: boolean | string;
|
||||
uid: string;
|
||||
}
|
||||
|
||||
export interface ClientRequest {
|
||||
intent: string;
|
||||
params: object;
|
||||
epic: string | boolean;
|
||||
confidence: number;
|
||||
}
|
||||
|
||||
export interface SessionState {
|
||||
key: string | boolean;
|
||||
newMessages: RenderMessage[];
|
||||
}
|
||||
|
||||
export interface WindowState {
|
||||
interface WindowState {
|
||||
width: number;
|
||||
height: number;
|
||||
x: number | null;
|
||||
y: number | null;
|
||||
}
|
||||
|
||||
export interface AuthRequest {
|
||||
key: boolean | string;
|
||||
usr: boolean | string;
|
||||
pwd: boolean | string;
|
||||
}
|
||||
|
||||
export interface Profile {
|
||||
interface Profile {
|
||||
crimataId: string;
|
||||
alias: string;
|
||||
initials: string;
|
||||
}
|
||||
|
||||
export interface AuthProtocol {
|
||||
token: null | string;
|
||||
profile: null | Profile;
|
||||
password?: string;
|
||||
email?: string;
|
||||
interface State {
|
||||
profile: Profile | null;
|
||||
messages
|
||||
}
|
||||
|
||||
export interface LogoutRequest {
|
||||
logout: boolean;
|
||||
}
|
||||
|
||||
export interface Annotation {
|
||||
text: string;
|
||||
context: string;
|
||||
uid: string;
|
||||
}
|
||||
|
||||
export interface StandardMessage {
|
||||
content: {
|
||||
text: boolean | string;
|
||||
audio: boolean | string;
|
||||
};
|
||||
context: string;
|
||||
modifier: string;
|
||||
interface LoginPayload {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,20 +2,39 @@
|
|||
|
||||
import { BrowserWindow, ipcMain } from "electron";
|
||||
import { createProtocol } from "vue-cli-plugin-electron-builder/lib";
|
||||
import { backgroundMitt } from '@/modules/emitter';
|
||||
import { RenderMessage, WindowState } from "@/types";
|
||||
import { loadWinState, saveToJson } from "./helpers";
|
||||
import { backgroundMitt } from './coposables/emitter';
|
||||
import { saveToJson } from "./coposables/json";
|
||||
import * as path from "path";
|
||||
const { autoUpdater } = require('electron-updater');
|
||||
|
||||
interface IpcRendererPayload {
|
||||
endpoint: string;
|
||||
message: RenderMessage | null;
|
||||
message: Message | null;
|
||||
}
|
||||
|
||||
let win: BrowserWindow | null;
|
||||
let winState: WindowState;
|
||||
|
||||
const loadWinState = (fileName: string): WindowState => {
|
||||
let state: WindowState;
|
||||
|
||||
try {
|
||||
state = JSON.parse(fs.readFileSync(configPath + fileName).toString());
|
||||
}
|
||||
|
||||
catch (error) {
|
||||
state = {
|
||||
width: 600,
|
||||
height: 500,
|
||||
x: null,
|
||||
y: null,
|
||||
}
|
||||
}
|
||||
|
||||
return state
|
||||
|
||||
}
|
||||
|
||||
// Called when a NavBar button is pressed.
|
||||
const onNavBar = (_event: any, action: string): void => {
|
||||
if (win) {
|
||||
|
|
@ -78,7 +97,7 @@ const onWindowDismount = (): void => {
|
|||
}
|
||||
|
||||
// function used by run.ts to create the main window.
|
||||
export async function createWindow(): Promise<void> {
|
||||
export default async function createWindow(): Promise<void> {
|
||||
return new Promise((resolve, _reject) => {
|
||||
|
||||
// avoid creating duplicate windows.
|
||||
|
|
@ -32,6 +32,7 @@
|
|||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"src/*.ts",
|
||||
"src/**/*.ts",
|
||||
"src/**/*.tsx",
|
||||
|
|
|
|||
Loading…
Reference in a new issue