diff --git a/.eslintrc.js b/.eslintrc.js
new file mode 100644
index 0000000..eaf98f7
--- /dev/null
+++ b/.eslintrc.js
@@ -0,0 +1,38 @@
+module.exports = {
+ root: true,
+ env: {
+ node: true
+ },
+ extends: [
+ "plugin:vue/vue3-essential",
+ "eslint:recommended",
+ "@vue/typescript/recommended"
+ ],
+ parserOptions: {
+ ecmaVersion: 2020
+ },
+ rules: {
+ "no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
+ "no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
+ "no-unused-vars": "off",
+ "@typescript-eslint/no-unused-vars": [
+ "error",
+ { "argsIgnorePattern": "^_" }
+ ]
+ },
+ overrides: [
+ {
+ files: [
+ "**/__tests__/*.{j,t}s?(x)",
+ "**/tests/unit/**/*.spec.{j,t}s?(x)",
+ "*.js"
+ ],
+ rules: {
+ "@typescript-eslint/no-var-requires": "off",
+ },
+ env: {
+ jest: true
+ }
+ }
+ ]
+};
diff --git a/.gitignore b/.gitignore
index f8cd566..20128fc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,7 @@
.DS_Store
-/node_modules
+node_modules
+/dist
+/crate/target/*
/.log
crash.log
@@ -11,6 +13,8 @@ crash.log
# Log files
npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
pnpm-debug.log*
# Editor directories and files
@@ -22,8 +26,12 @@ pnpm-debug.log*
*.sln
*.sw?
-# Dist
-*.app
+#Electron-builder output
+/dist_electron
-*.sublime-workspace
-*.sublime-project
+#Electron-builder config
+electron-builder.yml
+
+#Window and session states
+session.json
+window.json
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 0000000..2c0d22c
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,63 @@
+stages:
+ - Build
+ - Upload
+ - Release
+
+build:
+ stage: Build
+ tags:
+ # Use a macos runner to build.
+ - darwin
+ before_script:
+ - yarn
+ script:
+ - export VERSION=$(node -e "console.log(require('./package.json').version)")
+ - echo "VERSION=$VERSION" >> variables.env
+ - export APPNAME=$(node -e "console.log(require('./package.json').productName)")
+ - echo "APPNAME=$APPNAME" >> variables.env
+ - yarn build
+ artifacts:
+ reports:
+ dotenv: variables.env
+ name: $CI_COMMIT_REF_SLUG
+ paths:
+ - dist_electron/*.dmg
+ - dist_electron/*.zip
+ - dist_electron/*.yml
+ when: on_success
+ only:
+ - main
+
+variables:
+ PACKAGE: '${APPNAME}-${VERSION}.dmg'
+ PACKAGE_REGISTRY_URL: '${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/${APPNAME}/${VERSION}'
+
+# Upload package to gitlab registry.
+upload:
+ stage: Upload
+ needs:
+ - job: build
+ artifacts: true
+ rules:
+ - if: $CI_COMMIT_TAG
+ when: never # Do not run this job when a tag is created manually
+ - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH # Run this job when when commits are pushed to the default branch
+ script:
+ # Take the package we built and place it in the package registry.
+ - 'curl --header "JOB-TOKEN: $CI_JOB_TOKEN" --upload-file "dist_electron/${PACKAGE}" "${PACKAGE_REGISTRY_URL}/${PACKAGE}"'
+
+auto-release-master:
+ image: registry.gitlab.com/gitlab-org/release-cli
+ needs:
+ - job: build
+ artifacts: true
+ - job: upload
+ artifacts: true
+ stage: Release
+ rules:
+ - if: $CI_COMMIT_TAG
+ when: never # Do not run this job when a tag is created manually
+ - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH # Run this job when when commits are pushed to the default branch
+ script:
+ - echo "Release $VERSION"
+ - release-cli create --name "Release $VERSION" --tag-name v$VERSION --description "Release $CI_COMMIT_TITLE" --ref $CI_COMMIT_SHA --assets-link "{\"name\":\"${APPNAME}\",\"url\":\"${PACKAGE_REGISTRY_URL}/${PACKAGE}\"}"
diff --git a/.npmrc b/.npmrc
deleted file mode 100644
index ce46d51..0000000
--- a/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@crimata:registry=https://gitlab.com/api/v4/projects/28849281/packages/npm/
-//gitlab.com/api/v4/projects/28849281/packages/npm/:_authToken=${API_TOKEN}
\ No newline at end of file
diff --git a/README.md b/README.md
index e9fefb0..c13a8ab 100644
--- a/README.md
+++ b/README.md
@@ -27,40 +27,3 @@ yarn lint
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).
-
-## Sign and Notarize
-
-Prerequisites:
-* Apple Developer Account ($99)
-* Valid Apple Developer ID Application Certificate on keychain
-
-Clear debris from app before signing:
-
- xattr -cr Electron.app
-
-Sign and Notarize:
-
- node notarize.js
-
-Debug electron-osx-sign:
-
- export DEBUG=electron-osx-sign*
-
-### Some things to note:
-
-Notarize only when distributing outside the Mac App Store.
-
-#### Gatekeeper Assess
-Electron-osx-sign seems to have a bug where the sign will fail if "gatekeeper-assess" is true (default).
-
-#### Entitlements.plist
-Not to be confused with Info.plist, this file specifies entitlements the app can have in hardened runtime (e.g. Can I use the microphone?). Only including the ones Electron reccommends seems to work (including audio of course and omitting allow-unsigned-executable-memory).
-
-#### The Benifit of Electron OSX Sign
-Without this package, we would have to manually go in and figure out how to sign each level of the app. While it didn't work out of the box (gatekeeper-assess) it seems to be good otherwise. However, there is a bug where it puts multiple runtime flags on each command - not catastrophic though.
-
-#### Apple Password
-This must be an app specific password, not your normal Apple ID password.
-
-#### --Signiture-Flags OSX Sign
-Not exactly sure what this does or why its important, but Electron includes it.
diff --git a/babel.config.js b/babel.config.js
new file mode 100644
index 0000000..e955840
--- /dev/null
+++ b/babel.config.js
@@ -0,0 +1,5 @@
+module.exports = {
+ presets: [
+ '@vue/cli-plugin-babel/preset'
+ ]
+}
diff --git a/entitlements.plist b/entitlements.plist
deleted file mode 100644
index 0555172..0000000
--- a/entitlements.plist
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
- com.apple.security.cs.allow-jit
-
- com.apple.security.cs.debugger
-
- com.apple.security.device.audio-input
-
-
-
-
diff --git a/icon.png b/icon.png
deleted file mode 100644
index 2897b98..0000000
Binary files a/icon.png and /dev/null differ
diff --git a/jest.config.js b/jest.config.js
new file mode 100644
index 0000000..d7f13cc
--- /dev/null
+++ b/jest.config.js
@@ -0,0 +1,6 @@
+module.exports = {
+ preset: '@vue/cli-plugin-unit-jest/presets/typescript-and-babel',
+ transform: {
+ '^.+\\.vue$': 'vue-jest'
+ }
+}
diff --git a/jsconfig.json b/jsconfig.json
new file mode 100644
index 0000000..5df65d0
--- /dev/null
+++ b/jsconfig.json
@@ -0,0 +1,6 @@
+{
+ "typeAcquisition": {
+
+ "enable": true
+ }
+}
\ No newline at end of file
diff --git a/notarize.js b/notarize.js
deleted file mode 100644
index cd3b19a..0000000
--- a/notarize.js
+++ /dev/null
@@ -1,52 +0,0 @@
-const exec = require('child_process').exec;
-const sign = require("electron-osx-sign").signAsync;
-const notarize = require("electron-notarize").notarize;
-
-const app = "Crimata.app";
-const dir = `${app}/Contents/Resources/app`;
-
-const runShellCommand = (cmd) => (new Promise((resolve, reject) => {
- exec(cmd, (error, stdout, stderr) => {
- error ? reject(stderr) : resolve(stdout);
- });
-}));
-
-const signConfig = {
- "app": app,
- "hardened-runtime": true,
- "gatekeeper-assess": false,
- "signature-flags": "library",
- "entitlements": "entitlements.plist",
- "entitlements-inherit": "entitlements.plist",
-};
-
-const notarizeConfig = {
- appPath: app,
- appleId: "gundersena@crimata.com",
- appBundleId: "com.crimata.CrimataMessenger",
- appleIdPassword: process.env["AC_PASSWORD"]
-};
-
-(async function () {
-
- try
- {
- console.log("Building...");
- await runShellCommand(`rm -rf ${dir} && mkdir ${dir} && cp -r {package.json,src,node_modules} ${dir}
- `);
-
- console.log("Cleaning...");
- await runShellCommand(`xattr -cr ${app}`);
-
- console.log("Signing...");
- await sign(signConfig);
-
- console.log("Notarizing...");
- await notarize(notarizeConfig);
- }
- catch (e)
- {
- console.log(e);
- }
-
-})();
diff --git a/package-lock.json b/package-lock.json
deleted file mode 100644
index cc9403e..0000000
--- a/package-lock.json
+++ /dev/null
@@ -1,1059 +0,0 @@
-{
- "name": "Crimata",
- "version": "1.0.0",
- "lockfileVersion": 2,
- "requires": true,
- "packages": {
- "": {
- "name": "Crimata",
- "version": "1.0.0",
- "dependencies": {
- "@crimata/nodeaudio": "0.0.0",
- "axios": "^0.21.1",
- "base64-arraybuffer": "^1.0.1",
- "electron-store": "^8.0.0",
- "ws": "^7.3.1"
- },
- "devDependencies": {
- "electron-notarize": "^1.1.1",
- "electron-osx-sign": "^0.6.0"
- }
- },
- "node_modules/@crimata/nodeaudio": {
- "version": "0.0.0",
- "resolved": "https://gitlab.com/api/v4/projects/28849281/packages/npm/@crimata/nodeaudio/-/@crimata/nodeaudio-0.0.0.tgz",
- "integrity": "sha1-4lPe1QynEwebXcjezyAPhfwI+VM=",
- "hasInstallScript": true,
- "dependencies": {
- "bindings": "~1.2.1",
- "sleep": "^6.3.0"
- }
- },
- "node_modules/ajv": {
- "version": "8.6.2",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.6.2.tgz",
- "integrity": "sha512-9807RlWAgT564wT+DjeyU5OFMPjmzxVobvDFmNAhY+5zD6A2ly3jDp6sgnfyDtlIQ+7H97oc/DGCzzfu9rjw9w==",
- "dependencies": {
- "fast-deep-equal": "^3.1.1",
- "json-schema-traverse": "^1.0.0",
- "require-from-string": "^2.0.2",
- "uri-js": "^4.2.2"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/epoberezkin"
- }
- },
- "node_modules/ajv-formats": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz",
- "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
- "dependencies": {
- "ajv": "^8.0.0"
- },
- "peerDependencies": {
- "ajv": "^8.0.0"
- },
- "peerDependenciesMeta": {
- "ajv": {
- "optional": true
- }
- }
- },
- "node_modules/at-least-node": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
- "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==",
- "dev": true,
- "engines": {
- "node": ">= 4.0.0"
- }
- },
- "node_modules/atomically": {
- "version": "1.7.0",
- "resolved": "https://registry.npmjs.org/atomically/-/atomically-1.7.0.tgz",
- "integrity": "sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w==",
- "engines": {
- "node": ">=10.12.0"
- }
- },
- "node_modules/axios": {
- "version": "0.21.4",
- "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
- "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
- "dependencies": {
- "follow-redirects": "^1.14.0"
- }
- },
- "node_modules/base64-arraybuffer": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.1.tgz",
- "integrity": "sha512-vFIUq7FdLtjZMhATwDul5RZWv2jpXQ09Pd6jcVEOvIsqCWTRFD/ONHNfyOS8dA/Ippi5dsIgpyKWKZaAKZltbA==",
- "engines": {
- "node": ">= 0.6.0"
- }
- },
- "node_modules/base64-js": {
- "version": "1.5.1",
- "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
- "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
- "dev": true,
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/feross"
- },
- {
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
- },
- {
- "type": "consulting",
- "url": "https://feross.org/support"
- }
- ]
- },
- "node_modules/bindings": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.2.1.tgz",
- "integrity": "sha1-FK1hE4EtLTfXLme0ystLtyZQXxE="
- },
- "node_modules/bluebird": {
- "version": "3.7.2",
- "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
- "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==",
- "dev": true
- },
- "node_modules/buffer-alloc": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz",
- "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==",
- "dev": true,
- "dependencies": {
- "buffer-alloc-unsafe": "^1.1.0",
- "buffer-fill": "^1.0.0"
- }
- },
- "node_modules/buffer-alloc-unsafe": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz",
- "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==",
- "dev": true
- },
- "node_modules/buffer-fill": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz",
- "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=",
- "dev": true
- },
- "node_modules/compare-version": {
- "version": "0.1.2",
- "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz",
- "integrity": "sha1-AWLsLZNR9d3VmpICy6k1NmpyUIA=",
- "dev": true,
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/conf": {
- "version": "10.0.2",
- "resolved": "https://registry.npmjs.org/conf/-/conf-10.0.2.tgz",
- "integrity": "sha512-iyy4ArqyQ/yrzNASNBN+jaylu53JRuq0ztvL6KAWYHj4iN56BVuhy2SrzEEHBodNbacZr2Pd/4nWhoAwc66T1g==",
- "dependencies": {
- "ajv": "^8.1.0",
- "ajv-formats": "^2.0.2",
- "atomically": "^1.7.0",
- "debounce-fn": "^4.0.0",
- "dot-prop": "^6.0.1",
- "env-paths": "^2.2.1",
- "json-schema-typed": "^7.0.3",
- "onetime": "^5.1.2",
- "pkg-up": "^3.1.0",
- "semver": "^7.3.5"
- },
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/debounce-fn": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/debounce-fn/-/debounce-fn-4.0.0.tgz",
- "integrity": "sha512-8pYCQiL9Xdcg0UPSD3d+0KMlOjp+KGU5EPwYddgzQ7DATsg4fuUDjQtsYLmWjnk2obnNHgV3vE2Y4jejSOJVBQ==",
- "dependencies": {
- "mimic-fn": "^3.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/debug": {
- "version": "4.3.3",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz",
- "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==",
- "dev": true,
- "dependencies": {
- "ms": "2.1.2"
- },
- "engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
- }
- },
- "node_modules/dot-prop": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz",
- "integrity": "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==",
- "dependencies": {
- "is-obj": "^2.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/electron-notarize": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/electron-notarize/-/electron-notarize-1.1.1.tgz",
- "integrity": "sha512-kufsnqh86CTX89AYNG3NCPoboqnku/+32RxeJ2+7A4Rbm4bbOx0Nc7XTy3/gAlBfpj9xPAxHfhZLOHgfi6cJVw==",
- "dev": true,
- "dependencies": {
- "debug": "^4.1.1",
- "fs-extra": "^9.0.1"
- },
- "engines": {
- "node": ">= 10.0.0"
- }
- },
- "node_modules/electron-osx-sign": {
- "version": "0.6.0",
- "resolved": "https://registry.npmjs.org/electron-osx-sign/-/electron-osx-sign-0.6.0.tgz",
- "integrity": "sha512-+hiIEb2Xxk6eDKJ2FFlpofCnemCbjbT5jz+BKGpVBrRNT3kWTGs4DfNX6IzGwgi33hUcXF+kFs9JW+r6Wc1LRg==",
- "dev": true,
- "dependencies": {
- "bluebird": "^3.5.0",
- "compare-version": "^0.1.2",
- "debug": "^2.6.8",
- "isbinaryfile": "^3.0.2",
- "minimist": "^1.2.0",
- "plist": "^3.0.1"
- },
- "bin": {
- "electron-osx-flat": "bin/electron-osx-flat.js",
- "electron-osx-sign": "bin/electron-osx-sign.js"
- },
- "engines": {
- "node": ">=4.0.0"
- }
- },
- "node_modules/electron-osx-sign/node_modules/debug": {
- "version": "2.6.9",
- "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
- "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
- "dev": true,
- "dependencies": {
- "ms": "2.0.0"
- }
- },
- "node_modules/electron-osx-sign/node_modules/ms": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
- "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
- "dev": true
- },
- "node_modules/electron-store": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/electron-store/-/electron-store-8.0.0.tgz",
- "integrity": "sha512-ZgRPUZkfrrjWSqxZeaxu7lEvmYf6tgl49dLMqxXGnEmliSiwv3u4rJPG+mH3fBQP9PBqgSh4TCuxHZImMMUgWg==",
- "dependencies": {
- "conf": "^10.0.0",
- "type-fest": "^1.0.2"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/env-paths": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
- "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/fast-deep-equal": {
- "version": "3.1.3",
- "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
- "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
- },
- "node_modules/find-up": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
- "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
- "dependencies": {
- "locate-path": "^3.0.0"
- },
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/follow-redirects": {
- "version": "1.14.9",
- "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz",
- "integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==",
- "funding": [
- {
- "type": "individual",
- "url": "https://github.com/sponsors/RubenVerborgh"
- }
- ],
- "engines": {
- "node": ">=4.0"
- },
- "peerDependenciesMeta": {
- "debug": {
- "optional": true
- }
- }
- },
- "node_modules/fs-extra": {
- "version": "9.1.0",
- "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
- "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
- "dev": true,
- "dependencies": {
- "at-least-node": "^1.0.0",
- "graceful-fs": "^4.2.0",
- "jsonfile": "^6.0.1",
- "universalify": "^2.0.0"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/graceful-fs": {
- "version": "4.2.9",
- "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz",
- "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==",
- "dev": true
- },
- "node_modules/is-obj": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz",
- "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/isbinaryfile": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-3.0.3.tgz",
- "integrity": "sha512-8cJBL5tTd2OS0dM4jz07wQd5g0dCCqIhUxPIGtZfa5L6hWlvV5MHTITy/DBAsF+Oe2LS1X3krBUhNwaGUWpWxw==",
- "dev": true,
- "dependencies": {
- "buffer-alloc": "^1.2.0"
- },
- "engines": {
- "node": ">=0.6.0"
- }
- },
- "node_modules/json-schema-traverse": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
- "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
- },
- "node_modules/json-schema-typed": {
- "version": "7.0.3",
- "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-7.0.3.tgz",
- "integrity": "sha512-7DE8mpG+/fVw+dTpjbxnx47TaMnDfOI1jwft9g1VybltZCduyRQPJPvc+zzKY9WPHxhPWczyFuYa6I8Mw4iU5A=="
- },
- "node_modules/jsonfile": {
- "version": "6.1.0",
- "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
- "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
- "dev": true,
- "dependencies": {
- "universalify": "^2.0.0"
- },
- "optionalDependencies": {
- "graceful-fs": "^4.1.6"
- }
- },
- "node_modules/locate-path": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
- "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
- "dependencies": {
- "p-locate": "^3.0.0",
- "path-exists": "^3.0.0"
- },
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/lru-cache": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
- "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
- "dependencies": {
- "yallist": "^4.0.0"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/mimic-fn": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-3.1.0.tgz",
- "integrity": "sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/minimist": {
- "version": "1.2.5",
- "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
- "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
- "dev": true
- },
- "node_modules/ms": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
- "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
- "dev": true
- },
- "node_modules/nan": {
- "version": "2.15.0",
- "resolved": "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz",
- "integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ=="
- },
- "node_modules/onetime": {
- "version": "5.1.2",
- "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
- "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
- "dependencies": {
- "mimic-fn": "^2.1.0"
- },
- "engines": {
- "node": ">=6"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/onetime/node_modules/mimic-fn": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
- "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/p-limit": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
- "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
- "dependencies": {
- "p-try": "^2.0.0"
- },
- "engines": {
- "node": ">=6"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/p-locate": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
- "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
- "dependencies": {
- "p-limit": "^2.0.0"
- },
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/p-try": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
- "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/path-exists": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
- "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=",
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/pkg-up": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz",
- "integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==",
- "dependencies": {
- "find-up": "^3.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/plist": {
- "version": "3.0.4",
- "resolved": "https://registry.npmjs.org/plist/-/plist-3.0.4.tgz",
- "integrity": "sha512-ksrr8y9+nXOxQB2osVNqrgvX/XQPOXaU4BQMKjYq8PvaY1U18mo+fKgBSwzK+luSyinOuPae956lSVcBwxlAMg==",
- "dev": true,
- "dependencies": {
- "base64-js": "^1.5.1",
- "xmlbuilder": "^9.0.7"
- },
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/punycode": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
- "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/require-from-string": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
- "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/semver": {
- "version": "7.3.5",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
- "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
- "dependencies": {
- "lru-cache": "^6.0.0"
- },
- "bin": {
- "semver": "bin/semver.js"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/sleep": {
- "version": "6.3.0",
- "resolved": "https://registry.npmjs.org/sleep/-/sleep-6.3.0.tgz",
- "integrity": "sha512-+WgYl951qdUlb1iS97UvQ01pkauoBK9ML9I/CMPg41v0Ze4EyMlTgFTDDo32iYj98IYqxIjDMRd+L71lawFfpQ==",
- "hasInstallScript": true,
- "dependencies": {
- "nan": "^2.14.1"
- },
- "engines": {
- "node": ">=0.8.0"
- }
- },
- "node_modules/type-fest": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz",
- "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==",
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/universalify": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
- "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
- "dev": true,
- "engines": {
- "node": ">= 10.0.0"
- }
- },
- "node_modules/uri-js": {
- "version": "4.4.1",
- "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
- "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
- "dependencies": {
- "punycode": "^2.1.0"
- }
- },
- "node_modules/ws": {
- "version": "7.5.4",
- "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.4.tgz",
- "integrity": "sha512-zP9z6GXm6zC27YtspwH99T3qTG7bBFv2VIkeHstMLrLlDJuzA7tQ5ls3OJ1hOGGCzTQPniNJoHXIAOS0Jljohg==",
- "engines": {
- "node": ">=8.3.0"
- },
- "peerDependencies": {
- "bufferutil": "^4.0.1",
- "utf-8-validate": "^5.0.2"
- },
- "peerDependenciesMeta": {
- "bufferutil": {
- "optional": true
- },
- "utf-8-validate": {
- "optional": true
- }
- }
- },
- "node_modules/xmlbuilder": {
- "version": "9.0.7",
- "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz",
- "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=",
- "dev": true,
- "engines": {
- "node": ">=4.0"
- }
- },
- "node_modules/yallist": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
- "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
- }
- },
- "dependencies": {
- "@crimata/nodeaudio": {
- "version": "0.0.0",
- "resolved": "https://gitlab.com/api/v4/projects/28849281/packages/npm/@crimata/nodeaudio/-/@crimata/nodeaudio-0.0.0.tgz",
- "integrity": "sha1-4lPe1QynEwebXcjezyAPhfwI+VM=",
- "requires": {
- "bindings": "~1.2.1",
- "sleep": "^6.3.0"
- }
- },
- "ajv": {
- "version": "8.6.2",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.6.2.tgz",
- "integrity": "sha512-9807RlWAgT564wT+DjeyU5OFMPjmzxVobvDFmNAhY+5zD6A2ly3jDp6sgnfyDtlIQ+7H97oc/DGCzzfu9rjw9w==",
- "requires": {
- "fast-deep-equal": "^3.1.1",
- "json-schema-traverse": "^1.0.0",
- "require-from-string": "^2.0.2",
- "uri-js": "^4.2.2"
- }
- },
- "ajv-formats": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz",
- "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
- "requires": {
- "ajv": "^8.0.0"
- }
- },
- "at-least-node": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
- "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==",
- "dev": true
- },
- "atomically": {
- "version": "1.7.0",
- "resolved": "https://registry.npmjs.org/atomically/-/atomically-1.7.0.tgz",
- "integrity": "sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w=="
- },
- "axios": {
- "version": "0.21.4",
- "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
- "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
- "requires": {
- "follow-redirects": "^1.14.0"
- }
- },
- "base64-arraybuffer": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.1.tgz",
- "integrity": "sha512-vFIUq7FdLtjZMhATwDul5RZWv2jpXQ09Pd6jcVEOvIsqCWTRFD/ONHNfyOS8dA/Ippi5dsIgpyKWKZaAKZltbA=="
- },
- "base64-js": {
- "version": "1.5.1",
- "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
- "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
- "dev": true
- },
- "bindings": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.2.1.tgz",
- "integrity": "sha1-FK1hE4EtLTfXLme0ystLtyZQXxE="
- },
- "bluebird": {
- "version": "3.7.2",
- "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
- "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==",
- "dev": true
- },
- "buffer-alloc": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz",
- "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==",
- "dev": true,
- "requires": {
- "buffer-alloc-unsafe": "^1.1.0",
- "buffer-fill": "^1.0.0"
- }
- },
- "buffer-alloc-unsafe": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz",
- "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==",
- "dev": true
- },
- "buffer-fill": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz",
- "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=",
- "dev": true
- },
- "compare-version": {
- "version": "0.1.2",
- "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz",
- "integrity": "sha1-AWLsLZNR9d3VmpICy6k1NmpyUIA=",
- "dev": true
- },
- "conf": {
- "version": "10.0.2",
- "resolved": "https://registry.npmjs.org/conf/-/conf-10.0.2.tgz",
- "integrity": "sha512-iyy4ArqyQ/yrzNASNBN+jaylu53JRuq0ztvL6KAWYHj4iN56BVuhy2SrzEEHBodNbacZr2Pd/4nWhoAwc66T1g==",
- "requires": {
- "ajv": "^8.1.0",
- "ajv-formats": "^2.0.2",
- "atomically": "^1.7.0",
- "debounce-fn": "^4.0.0",
- "dot-prop": "^6.0.1",
- "env-paths": "^2.2.1",
- "json-schema-typed": "^7.0.3",
- "onetime": "^5.1.2",
- "pkg-up": "^3.1.0",
- "semver": "^7.3.5"
- }
- },
- "debounce-fn": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/debounce-fn/-/debounce-fn-4.0.0.tgz",
- "integrity": "sha512-8pYCQiL9Xdcg0UPSD3d+0KMlOjp+KGU5EPwYddgzQ7DATsg4fuUDjQtsYLmWjnk2obnNHgV3vE2Y4jejSOJVBQ==",
- "requires": {
- "mimic-fn": "^3.0.0"
- }
- },
- "debug": {
- "version": "4.3.3",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz",
- "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==",
- "dev": true,
- "requires": {
- "ms": "2.1.2"
- }
- },
- "dot-prop": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz",
- "integrity": "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==",
- "requires": {
- "is-obj": "^2.0.0"
- }
- },
- "electron-notarize": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/electron-notarize/-/electron-notarize-1.1.1.tgz",
- "integrity": "sha512-kufsnqh86CTX89AYNG3NCPoboqnku/+32RxeJ2+7A4Rbm4bbOx0Nc7XTy3/gAlBfpj9xPAxHfhZLOHgfi6cJVw==",
- "dev": true,
- "requires": {
- "debug": "^4.1.1",
- "fs-extra": "^9.0.1"
- }
- },
- "electron-osx-sign": {
- "version": "0.6.0",
- "resolved": "https://registry.npmjs.org/electron-osx-sign/-/electron-osx-sign-0.6.0.tgz",
- "integrity": "sha512-+hiIEb2Xxk6eDKJ2FFlpofCnemCbjbT5jz+BKGpVBrRNT3kWTGs4DfNX6IzGwgi33hUcXF+kFs9JW+r6Wc1LRg==",
- "dev": true,
- "requires": {
- "bluebird": "^3.5.0",
- "compare-version": "^0.1.2",
- "debug": "^2.6.8",
- "isbinaryfile": "^3.0.2",
- "minimist": "^1.2.0",
- "plist": "^3.0.1"
- },
- "dependencies": {
- "debug": {
- "version": "2.6.9",
- "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
- "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
- "dev": true,
- "requires": {
- "ms": "2.0.0"
- }
- },
- "ms": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
- "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
- "dev": true
- }
- }
- },
- "electron-store": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/electron-store/-/electron-store-8.0.0.tgz",
- "integrity": "sha512-ZgRPUZkfrrjWSqxZeaxu7lEvmYf6tgl49dLMqxXGnEmliSiwv3u4rJPG+mH3fBQP9PBqgSh4TCuxHZImMMUgWg==",
- "requires": {
- "conf": "^10.0.0",
- "type-fest": "^1.0.2"
- }
- },
- "env-paths": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
- "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="
- },
- "fast-deep-equal": {
- "version": "3.1.3",
- "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
- "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
- },
- "find-up": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
- "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
- "requires": {
- "locate-path": "^3.0.0"
- }
- },
- "follow-redirects": {
- "version": "1.14.9",
- "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz",
- "integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w=="
- },
- "fs-extra": {
- "version": "9.1.0",
- "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
- "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
- "dev": true,
- "requires": {
- "at-least-node": "^1.0.0",
- "graceful-fs": "^4.2.0",
- "jsonfile": "^6.0.1",
- "universalify": "^2.0.0"
- }
- },
- "graceful-fs": {
- "version": "4.2.9",
- "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz",
- "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==",
- "dev": true
- },
- "is-obj": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz",
- "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w=="
- },
- "isbinaryfile": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-3.0.3.tgz",
- "integrity": "sha512-8cJBL5tTd2OS0dM4jz07wQd5g0dCCqIhUxPIGtZfa5L6hWlvV5MHTITy/DBAsF+Oe2LS1X3krBUhNwaGUWpWxw==",
- "dev": true,
- "requires": {
- "buffer-alloc": "^1.2.0"
- }
- },
- "json-schema-traverse": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
- "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
- },
- "json-schema-typed": {
- "version": "7.0.3",
- "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-7.0.3.tgz",
- "integrity": "sha512-7DE8mpG+/fVw+dTpjbxnx47TaMnDfOI1jwft9g1VybltZCduyRQPJPvc+zzKY9WPHxhPWczyFuYa6I8Mw4iU5A=="
- },
- "jsonfile": {
- "version": "6.1.0",
- "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
- "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
- "dev": true,
- "requires": {
- "graceful-fs": "^4.1.6",
- "universalify": "^2.0.0"
- }
- },
- "locate-path": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
- "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
- "requires": {
- "p-locate": "^3.0.0",
- "path-exists": "^3.0.0"
- }
- },
- "lru-cache": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
- "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
- "requires": {
- "yallist": "^4.0.0"
- }
- },
- "mimic-fn": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-3.1.0.tgz",
- "integrity": "sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ=="
- },
- "minimist": {
- "version": "1.2.5",
- "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
- "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
- "dev": true
- },
- "ms": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
- "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
- "dev": true
- },
- "nan": {
- "version": "2.15.0",
- "resolved": "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz",
- "integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ=="
- },
- "onetime": {
- "version": "5.1.2",
- "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
- "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
- "requires": {
- "mimic-fn": "^2.1.0"
- },
- "dependencies": {
- "mimic-fn": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
- "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="
- }
- }
- },
- "p-limit": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
- "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
- "requires": {
- "p-try": "^2.0.0"
- }
- },
- "p-locate": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
- "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
- "requires": {
- "p-limit": "^2.0.0"
- }
- },
- "p-try": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
- "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="
- },
- "path-exists": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
- "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU="
- },
- "pkg-up": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz",
- "integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==",
- "requires": {
- "find-up": "^3.0.0"
- }
- },
- "plist": {
- "version": "3.0.4",
- "resolved": "https://registry.npmjs.org/plist/-/plist-3.0.4.tgz",
- "integrity": "sha512-ksrr8y9+nXOxQB2osVNqrgvX/XQPOXaU4BQMKjYq8PvaY1U18mo+fKgBSwzK+luSyinOuPae956lSVcBwxlAMg==",
- "dev": true,
- "requires": {
- "base64-js": "^1.5.1",
- "xmlbuilder": "^9.0.7"
- }
- },
- "punycode": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
- "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
- },
- "require-from-string": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
- "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="
- },
- "semver": {
- "version": "7.3.5",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
- "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
- "requires": {
- "lru-cache": "^6.0.0"
- }
- },
- "sleep": {
- "version": "6.3.0",
- "resolved": "https://registry.npmjs.org/sleep/-/sleep-6.3.0.tgz",
- "integrity": "sha512-+WgYl951qdUlb1iS97UvQ01pkauoBK9ML9I/CMPg41v0Ze4EyMlTgFTDDo32iYj98IYqxIjDMRd+L71lawFfpQ==",
- "requires": {
- "nan": "^2.14.1"
- }
- },
- "type-fest": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz",
- "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA=="
- },
- "universalify": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
- "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
- "dev": true
- },
- "uri-js": {
- "version": "4.4.1",
- "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
- "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
- "requires": {
- "punycode": "^2.1.0"
- }
- },
- "ws": {
- "version": "7.5.4",
- "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.4.tgz",
- "integrity": "sha512-zP9z6GXm6zC27YtspwH99T3qTG7bBFv2VIkeHstMLrLlDJuzA7tQ5ls3OJ1hOGGCzTQPniNJoHXIAOS0Jljohg==",
- "requires": {}
- },
- "xmlbuilder": {
- "version": "9.0.7",
- "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz",
- "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=",
- "dev": true
- },
- "yallist": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
- "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
- }
- }
-}
diff --git a/package.json b/package.json
index 6b6c329..24d13b1 100644
--- a/package.json
+++ b/package.json
@@ -1,25 +1,104 @@
{
"name": "Crimata",
- "productName": "crimata-messenger",
- "version": "1.0.0",
+ "version": "0.9.9",
"private": true,
- "description": "Cross-platform messenger app (Electron, Vue3)",
+ "description": "Cross-platform messenger application built with electron, vue3, and TS.",
"author": {
- "name": "Andrew Gundersen"
+ "name": "Enrique Hernandez"
},
- "main": "src/main.js",
"scripts": {
- "start": "electron ."
+ "build": "vue-cli-service electron:build",
+ "dev": "vue-cli-service electron:serve",
+ "postinstall": "electron-builder install-app-deps",
+ "postuninstall": "electron-builder install-app-deps"
},
+ "main": "init.js",
"dependencies": {
- "@crimata/nodeaudio": "0.0.0",
+ "@google-cloud/speech": "^4.2.0",
+ "@types/animejs": "^3.1.2",
+ "@types/bindings": "^1.3.0",
+ "@types/dom-mediacapture-record": "^1.0.7",
+ "@types/node": "^14.14.25",
+ "@types/uuid": "^8.3.0",
+ "@types/ws": "^7.2.7",
+ "animejs": "^3.2.0",
"axios": "^0.21.1",
- "base64-arraybuffer": "^1.0.1",
+ "core-js": "^3.6.5",
+ "electron-is-dev": "^2.0.0",
"electron-store": "^8.0.0",
+ "electron-updater": "^4.3.8",
+ "mitt": "^2.1.0",
+ "naudiodon": "^2.3.2",
+ "node-record-lpcm16": "^1.0.1",
+ "update-electron-app": "^2.0.1",
+ "uuid": "^8.3.2",
+ "vue": "^3.0.0-0",
+ "vue-router": "^4.0.0-0",
+ "vuex": "^4.0.0-0",
"ws": "^7.3.1"
},
"devDependencies": {
- "electron-notarize": "^1.1.1",
- "electron-osx-sign": "^0.6.0"
+ "@types/axios": "^0.14.0",
+ "@types/electron-devtools-installer": "^2.2.0",
+ "@types/jest": "^24.0.19",
+ "@typescript-eslint/eslint-plugin": "^2.33.0",
+ "@typescript-eslint/parser": "^2.33.0",
+ "@vue/cli-plugin-babel": "~4.5.0",
+ "@vue/cli-plugin-eslint": "~4.5.0",
+ "@vue/cli-plugin-router": "~4.5.0",
+ "@vue/cli-plugin-typescript": "~4.5.0",
+ "@vue/cli-plugin-unit-jest": "~4.5.0",
+ "@vue/cli-plugin-vuex": "~4.5.0",
+ "@vue/cli-service": "~4.5.0",
+ "@vue/compiler-sfc": "^3.0.0-0",
+ "@vue/eslint-config-typescript": "^5.0.2",
+ "@vue/test-utils": "^2.0.0-0",
+ "@wasm-tool/wasm-pack-plugin": "^1.3.1",
+ "electron": "^9.0.0",
+ "electron-devtools-installer": "^3.1.0",
+ "electron-log": "^4.3.4",
+ "eslint": "^6.7.2",
+ "eslint-plugin-vue": "^7.0.0-0",
+ "lint-staged": "^9.5.0",
+ "node-sass": "^4.12.0",
+ "optimize-wasm-webpack-plugin": "^1.0.12",
+ "sass-loader": "^8.0.2",
+ "spectron": "11.0.0",
+ "typescript": "~3.9.3",
+ "vue-cli-plugin-electron-builder": "~2.0.0-rc.6",
+ "vue-jest": "^5.0.0-0"
+ },
+ "vue": {
+ "lintOnSave": false,
+ "pluginOptions": {
+ "electronBuilder": {
+ "mainProcessFile": "./src/init.ts",
+ "rendererProcessFile": "./src/render/main.ts",
+ "preload": "./src/render/preload.ts",
+ "builderOptions": {
+ "appId": "com.crimata.ElectronUpdaterApp",
+ "artifactName": "${productName}-${version}.${ext}",
+ "publish": {
+ "provider": "generic",
+ "url": "https://gitlab.com/api/v4/projects/25637892/jobs/artifacts/main/raw/dist_electron?job=build"
+ }
+ }
+ }
+ }
+ },
+ "gitHooks": {
+ "pre-commit": "lint-staged"
+ },
+ "lint-staged": {
+ "*.{js,jsx,vue,ts,tsx}": [
+ "vue-cli-service lint",
+ "git add"
+ ]
+ },
+ "productName": "crimata-messenger",
+ "repository": {
+ "type": "git",
+ "url": "https://gitlab.com/crimata/electron-app.git",
+ "release": "latest"
}
}
diff --git a/public/index.html b/public/index.html
new file mode 100644
index 0000000..8f79d27
--- /dev/null
+++ b/public/index.html
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+ <%= htmlWebpackPlugin.options.title %>
+
+
+
+
+
+
+
diff --git a/src/account.js b/src/account.js
deleted file mode 100644
index e339db2..0000000
--- a/src/account.js
+++ /dev/null
@@ -1,41 +0,0 @@
-const { ipcMain } = require("electron");
-
-const { post } = require("./api");
-const store = require("./utils/store");
-const { launchSession, endSession } = require("./session");
-const { backgroundMitt, ipcEmit } = require("./utils/emitter");
-
-async function auth(_e, creds)
-{
- const { error, data } = await post("/auth", creds);
-
- if (error)
- {
- return data;
- }
-
- store.set("account", data);
- ipcEmit("account", data);
- launchSession(data);
-};
-
-function logout(_e, reason)
-{
- store.delete("account");
- ipcEmit("account", false, reason);
- endSession();
-};
-
-function initAccount()
-{
- ipcMain.handle("auth", auth);
- ipcMain.on("logout", logout);
-
- const account = store.get("account");
- if (account) launchSession(account);
-}
-
-backgroundMitt.on("logout", (reason) => logout(null, reason));
-
-
-exports.initAccount = initAccount;
diff --git a/src/account.ts b/src/account.ts
new file mode 100644
index 0000000..581da0e
--- /dev/null
+++ b/src/account.ts
@@ -0,0 +1,95 @@
+
+import { postAuth, postLogin, postLogout } from "@/api/account";
+import { endSession, launchSession } from "@/session";
+import { getToken, setToken, setProfile, getProfile, clearStore } from "./store";
+import { parseAuthRes } from "./auth";
+import { ipcEmit } from "@/composables/useEmitter";
+
+export const accountAuth = async (): Promise => {
+
+ /* attempt to get a login token from the store */
+ const token = getToken();
+
+ /* try to login with it, returns platform secret and new token on success */
+ if (token) {
+ try {
+
+ const res = await postAuth(token);
+
+ const parsed = parseAuthRes(res);
+
+ setToken(parsed.token)
+ setProfile(parsed.profile);
+
+ return {
+ profile: parsed.profile,
+ token: parsed.token
+ };
+
+ } catch(e) {
+ console.log('[ACCOUNT]', e);
+ clearStore();
+ throw(new Error('Failed to authenticate.'));
+
+ }
+ } else {
+ throw(new Error('Unable to authenticate.'));
+ }
+};
+
+export const accountLogin: IpcHandlerCallback = async (payload) => {
+ const account = payload as AccountCredentials;
+ try {
+
+ // attempt login with email password
+ const res = await postLogin(account.email, account.password);
+ const parsed = parseAuthRes(res);
+
+ // save jwt token and profile
+ setToken(parsed.token);
+ setProfile(parsed.profile);
+
+ // launch session
+ launchSession(parsed.token);
+
+ // return profile to renderer
+ return parsed.profile;
+
+ } catch(e) {
+ clearStore();
+ throw e;
+ }
+}
+
+
+export const accountLogout = async (): Promise => {
+
+ try {
+ // post logout to backend
+ await postLogout();
+
+ // remove key and crimataId
+ clearStore();
+
+ // kill crimata platform session
+ endSession();
+
+ return;
+
+ } catch(e) {
+ console.log('[ACCOUNT]', e);
+ return (new Error('Failed to logout. Please try again.'));
+ }
+
+}
+
+export const updateAppState = (): void => {
+
+ const profile = getProfile();
+
+ ipcEmit("set-profile", profile);
+
+ // ipcEmit('messages') etc
+
+}
+
diff --git a/src/api.js b/src/api.js
deleted file mode 100644
index 785c304..0000000
--- a/src/api.js
+++ /dev/null
@@ -1,38 +0,0 @@
-const axios = require('axios').default;
-
-const config = require("./config");
-
-
-async function post(route, body)
-{
- let error;
- let data;
-
- try
- {
- const res = await axios.post(config.API + route, body);
- data = res.data;
- error = false;
- }
- catch (e)
- {
- if (e.response)
- {
- data = e.response.data;
- }
- else if (e.request)
- {
- data = "Can't connect to the server."
- }
- else
- {
- data = "Unknown error occured."
- }
-
- error = true;
- }
-
- return { error, data };
-}
-
-exports.post = post;
diff --git a/src/api/account.ts b/src/api/account.ts
new file mode 100644
index 0000000..45baca2
--- /dev/null
+++ b/src/api/account.ts
@@ -0,0 +1,31 @@
+
+import useHttp from "@/composables/useHttp";
+import axios from "axios";
+import {config} from "@/config";
+
+const { post } = useHttp();
+
+export const postAuth = async (token: string) => (
+ await axios({
+ url: config.BUSINESS_URL + config.BUSINESS_PREFIX + '/account/authenticate',
+ headers: {
+ Cookie: `crimataCookie=${token}`
+ },
+ method: 'POST',
+ })
+);
+
+
+export const postLogin = async (email: string, password: string) => (
+ await post('/account/login', { email, password })
+);
+
+
+export const postLogout =
+ async (): Promise => (await post('/account/logout'));
+
+
+
+
+
+
diff --git a/src/assets/icon.icns b/src/assets/icon.icns
deleted file mode 100644
index b173533..0000000
Binary files a/src/assets/icon.icns and /dev/null differ
diff --git a/src/assets/tray/000.png b/src/assets/tray/000.png
deleted file mode 100644
index e6d5d32..0000000
Binary files a/src/assets/tray/000.png and /dev/null differ
diff --git a/src/assets/tray/000@2x.png b/src/assets/tray/000@2x.png
deleted file mode 100644
index 6d61d7f..0000000
Binary files a/src/assets/tray/000@2x.png and /dev/null differ
diff --git a/src/assets/tray/001.png b/src/assets/tray/001.png
deleted file mode 100644
index 8d65b38..0000000
Binary files a/src/assets/tray/001.png and /dev/null differ
diff --git a/src/assets/tray/001@2x.png b/src/assets/tray/001@2x.png
deleted file mode 100644
index b7f0e2c..0000000
Binary files a/src/assets/tray/001@2x.png and /dev/null differ
diff --git a/src/assets/tray/010.png b/src/assets/tray/010.png
deleted file mode 100644
index bc5718c..0000000
Binary files a/src/assets/tray/010.png and /dev/null differ
diff --git a/src/assets/tray/010@2x.png b/src/assets/tray/010@2x.png
deleted file mode 100644
index 6b97193..0000000
Binary files a/src/assets/tray/010@2x.png and /dev/null differ
diff --git a/src/assets/tray/011.png b/src/assets/tray/011.png
deleted file mode 100644
index c2f8612..0000000
Binary files a/src/assets/tray/011.png and /dev/null differ
diff --git a/src/assets/tray/011@2x.png b/src/assets/tray/011@2x.png
deleted file mode 100644
index befcd5f..0000000
Binary files a/src/assets/tray/011@2x.png and /dev/null differ
diff --git a/src/assets/tray/100.png b/src/assets/tray/100.png
deleted file mode 100644
index 99df7df..0000000
Binary files a/src/assets/tray/100.png and /dev/null differ
diff --git a/src/assets/tray/100@2x.png b/src/assets/tray/100@2x.png
deleted file mode 100644
index 2d26144..0000000
Binary files a/src/assets/tray/100@2x.png and /dev/null differ
diff --git a/src/assets/tray/101.png b/src/assets/tray/101.png
deleted file mode 100644
index 523bc93..0000000
Binary files a/src/assets/tray/101.png and /dev/null differ
diff --git a/src/assets/tray/101@2x.png b/src/assets/tray/101@2x.png
deleted file mode 100644
index 91a15be..0000000
Binary files a/src/assets/tray/101@2x.png and /dev/null differ
diff --git a/src/assets/tray/110.png b/src/assets/tray/110.png
deleted file mode 100644
index ddd2524..0000000
Binary files a/src/assets/tray/110.png and /dev/null differ
diff --git a/src/assets/tray/110@2x.png b/src/assets/tray/110@2x.png
deleted file mode 100644
index fd073dd..0000000
Binary files a/src/assets/tray/110@2x.png and /dev/null differ
diff --git a/src/assets/tray/111.png b/src/assets/tray/111.png
deleted file mode 100644
index d2dd686..0000000
Binary files a/src/assets/tray/111.png and /dev/null differ
diff --git a/src/assets/tray/111@2x.png b/src/assets/tray/111@2x.png
deleted file mode 100644
index 2bc77fd..0000000
Binary files a/src/assets/tray/111@2x.png and /dev/null differ
diff --git a/src/audio.js b/src/audio.js
deleted file mode 100644
index 73223b2..0000000
--- a/src/audio.js
+++ /dev/null
@@ -1,134 +0,0 @@
-const nodeAudio = require("@crimata/nodeaudio");
-const { globalShortcut, ipcMain } = require("electron");
-
-const { sendMessage } = require("./io");
-const { updateTray } = require("./tray");
-const { encode, decode } = require("./codec");
-const { backgroundMitt, ipcEmit } = require("./utils/emitter");
-
-let inputDevice;
-let outputDevice;
-
-let setWriteId;
-let setStreamsId;
-
-let autoStopId; /* keep track of recording time */
-
-let playbackId; /* UID of the message being played */
-
-const streamState = { rec: false, pb: false };
-
-const /** @type {Int16Array[]} */ chunks = [];
-
-backgroundMitt.on("data", (int16Arr) => {
- if (streamState.rec) chunks.push(int16Arr);
-});
-
-backgroundMitt.on("write", (int16Arr) => {
- clearTimeout(setWriteId);
- setWriteId = setTimeout(() => {
- setPlaybackStatus(false);
- }, 500);
-});
-
-function setStreams()
-{
- const defaultInput = nodeAudio.core.GetDefaultInputDevice();
- const defaultOutput = nodeAudio.core.GetDefaultOutputDevice();
-
- if (inputDevice !== defaultInput)
- {
- inputDevice = defaultInput;
- nodeAudio.core.CloseInputStream(inputDevice);
- nodeAudio.core.OpenInputStream(inputDevice);
- }
-
- if (outputDevice !== defaultOutput)
- {
- outputDevice = defaultOutput;
- nodeAudio.core.CloseOutputStream(outputDevice);
- nodeAudio.core.OpenOutputStream(outputDevice);
- }
-}
-
-function startRecording()
-{
- setRecordingStatus(true);
-
- /* 15s recording time limit */
- autoStopId = setTimeout(stopRecording, 15000);
-}
-
-function stopRecording()
-{
- if (autoStopId)
- {
- clearTimeout(autoStopId);
- }
-
- setRecordingStatus(false);
-
- sendMessage({
- category: "audio",
- text: null,
- blob: encode(nodeAudio.utils.mergeChunks(chunks).buffer)
- });
-
- chunks.length = 0;
-}
-
-function initAudio()
-{
- nodeAudio.core.Initialize(backgroundMitt.emit.bind(backgroundMitt));
- setStreamsId = setInterval(setStreams, 2000);
-
- const res = globalShortcut.register('CommandOrControl+Return', () => {
- streamState.rec ? stopRecording() : startRecording();
- });
-
- if (!res) throw new Error("Failed to register recording shortcut");
-}
-
-function playback(base64String, id)
-{
- /* terminate any current playback */
- nodeAudio.core.CancelPlayback();
- ipcEmit("playback", playbackId, false);
-
- playbackId = id;
- setPlaybackStatus(true);
- nodeAudio.core.WriteToOutputStream(decode(base64String));
-}
-
-function terminateAudio()
-{
- globalShortcut.unregisterAll();
-
- /* Only terminate PA if initialized */
- if (setStreamsId)
- {
- clearInterval(setStreamsId);
- nodeAudio.core.Terminate();
- }
-}
-
-function setRecordingStatus(status)
-{
- streamState.rec = status;
- ipcEmit("record", status);
- updateTray("recording", streamState.rec);
-}
-
-function setPlaybackStatus(status)
-{
- console.log("Setting playback status: ", status);
-
- streamState.pb = status;
- ipcEmit("playback", playbackId, status);
- updateTray("playback", status);
-}
-
-exports.initAudio = initAudio;
-exports.terminateAudio = terminateAudio;
-exports.playback = playback;
-exports.streamState = streamState;
diff --git a/src/audio.ts b/src/audio.ts
new file mode 100644
index 0000000..7ae2562
--- /dev/null
+++ b/src/audio.ts
@@ -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 const collect: IpcListenerCallback = (chunk) => {
+ if (chunk) buffer.push(chunk);
+}
+
+// return audio and clear buffer
+export const flush = async () => {
+ const bufferCopy = buffer;
+ buffer = [];
+ return bufferCopy;
+}
+
+// import { backgroundMitt } from '@/modules/emitter';
+// const portAudio = require('naudiodon');
+
+// // Audio in and out stream objects.
+// let ai: typeof portAudio.AudioIO | boolean = false;
+// let ao: typeof portAudio.AudioIO | boolean = false;
+
+// // Whether activly recording.
+// let record = false;
+
+// const audioContainer = {
+// input: '',
+// }
+
+// const audioOptions = {
+// channelCount: 1,
+// sampleFormat: 16,
+// sampleRate: 16000,
+// deviceId: -1,
+// closeOnError: false,
+// }
+
+// export const toggleRecord = (): void => { record = !record };
+
+
+// export const fetchAudioInput = (): Promise => (
+
+// new Promise((resolve, reject) => {
+
+// try {
+// resolve(audioContainer.input);
+// toggleRecord();
+// } catch (e) {
+// reject(new Error('Failed to fetch the audio.'))
+// }
+
+// })
+// )
+
+
+// // Main audio function run by run.ts module.
+// export function initAudioIO(): void {
+// console.log("AUDIO:Starting io streams.")
+
+// if (!ai) {
+
+// // Initialize and start input stream.
+// ai = new portAudio.AudioIO({ inOptions: audioOptions });
+// ai.setEncoding("hex");
+// ai.start();
+
+// // On each data chunk...
+// ai.on('data', (chunk: string) => {
+
+// // If recording, we capture the data.
+// if (record) {
+// console.log('AUDIO:Recording...')
+// audioContainer.input += chunk;
+// }
+
+// // Else, we don't capture and also clear audioContainer.
+// else {
+// if (audioContainer.input.length) {
+// audioContainer.input = "";
+// }
+// }
+
+// });
+// }
+
+// if (!ao) {
+
+// // Initialize and start input stream.
+// ao = new portAudio.AudioIO({ outOptions: audioOptions });
+// ao.start();
+
+// }
+// }
+
+
+// // ---Audio playback--------------------------------------------
+
+// // Split Buffer into an array of len-sized Buffers.
+// function bufSplit(buf: Buffer, len: number): Array {
+// const chunks = [];
+// let i = 0;
+// let L = len;
+
+// while(i < buf.byteLength) {
+// chunks.push(buf.slice(i, L));
+// i = L;
+// L += len;
+// }
+
+// return chunks;
+// }
+
+// // Audio playback.
+// export function play(input: string): void {
+
+// // Format the audio.
+// const audio = bufSplit(
+// Buffer.from(input as string, 'hex'),
+// 8192
+// );
+
+// // Called on end of write.
+// const callback = () => {
+
+// // We stop audio playback anim.
+// backgroundMitt.emit('ipc-renderer', {
+// endpoint: 'stop-playback-anim'
+// });
+
+// }
+
+// write();
+
+// // Iterate through audio array and write buffers to portAudio writable.
+// function write() {
+// let chunk: Buffer;
+// let ok = true;
+// let i = 0;
+
+// do {
+// chunk = audio[i];
+// if (i === audio.length - 1) {
+// // write last chunk.
+// ao.write(chunk, null, callback);
+// } else {
+// // check for backpreassure.
+// ok = ao.write(chunk, null);
+// }
+// i++;
+// } while (i < audio.length && ok);
+
+// if (i < audio.length) {
+// // Had to stop early!
+// // Write some more once it drains.
+// ao.once('drain', write);
+// }
+// }
+// }
+
+// // -------------------------------------------------------------
+
+// // Get's called on window close.
+// export async function stopStream() {
+// console.log("AUDIO:Stopping audio stream.")
+// if (ai) {
+// try {
+// await ai.quit()
+// } catch(e){
+// console.log('AUDIO: Failed to shutdown audio input.');
+// throw e;
+// }
+// }
+// if (ao) {
+// try {
+// await ao.quit()
+// } catch(e){
+// console.log('AUDIO: Failed to shutdown audio output.');
+// throw e;
+// }
+// }
+// }
+
+
diff --git a/src/auth.ts b/src/auth.ts
new file mode 100644
index 0000000..14ed36a
--- /dev/null
+++ b/src/auth.ts
@@ -0,0 +1,13 @@
+
+export const parseAuthRes = (authRes: any) => {
+ const token = authRes.headers['set-cookie'][0].split(";")[0].split("=")[1] as string;
+ const profile = authRes.data as Profile;
+ return {
+ token,
+ profile
+ }
+};
+
+
+
+
diff --git a/src/codec.js b/src/codec.js
deleted file mode 100644
index 3a9a8bf..0000000
--- a/src/codec.js
+++ /dev/null
@@ -1,13 +0,0 @@
-const { ipcMain } = require("electron");
-const { encode, decode } = require("base64-arraybuffer");
-
-ipcMain.handle("encode", (_e, arrayBuffer) => {
- return encode(arrayBuffer);
-});
-
-ipcMain.handle("decode", (_e, b64string) => {
- return decode(b64string);
-});
-
-exports.encode = encode;
-exports.decode = decode;
\ No newline at end of file
diff --git a/src/composables/autoUpdate.ts b/src/composables/autoUpdate.ts
new file mode 100644
index 0000000..1a6b780
--- /dev/null
+++ b/src/composables/autoUpdate.ts
@@ -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()
+ })
+
+})
\ No newline at end of file
diff --git a/src/composables/useEmitter.ts b/src/composables/useEmitter.ts
new file mode 100644
index 0000000..f95f328
--- /dev/null
+++ b/src/composables/useEmitter.ts
@@ -0,0 +1,16 @@
+
+// Backend emitter
+//
+const EventEmitter = require('events');
+class BackgroundMitt extends EventEmitter { }
+
+export const backgroundMitt = new BackgroundMitt();
+
+export const ipcEmit = (channel: string, payload: T) => {
+ backgroundMitt.emit('ipc-renderer', {
+ channel,
+ payload
+ });
+};
+
+
diff --git a/src/composables/useHttp.ts b/src/composables/useHttp.ts
new file mode 100644
index 0000000..6b9ee56
--- /dev/null
+++ b/src/composables/useHttp.ts
@@ -0,0 +1,51 @@
+
+import axios, { AxiosRequestConfig } from 'axios';
+import {config} from "@/config";
+
+const baseURL = config.BUSINESS_URL + config.BUSINESS_PREFIX;
+
+interface Request {
+ endpoint: string;
+ query?: Record;
+ config?: Record;
+}
+
+const makeQuery = (reqQuery: Record) => {
+
+ let result = '';
+
+ result = '?' + Object.entries(reqQuery)
+ .map(([ key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
+ .join('&')
+
+ return result;
+};
+
+
+export default function useHttp() {
+
+ const api = axios.create({
+ baseURL,
+ withCredentials: true,
+ });
+
+
+ const post = async (endpoint: string, payload?: Record): Promise => (
+ await api.post(endpoint, payload)
+ )
+
+
+ const get = async (req: Request) => {
+
+ if (req.query) {
+ req.endpoint += makeQuery(req.query);
+ }
+
+ const res = await api.get(req.endpoint, req.config);
+ return res;
+ };
+
+ return {
+ get, post
+ }
+}
diff --git a/src/composables/useIpcMain.ts b/src/composables/useIpcMain.ts
new file mode 100644
index 0000000..ebfc4bc
--- /dev/null
+++ b/src/composables/useIpcMain.ts
@@ -0,0 +1,82 @@
+
+import { ipcMain, IpcMainInvokeEvent, IpcMainEvent } from "electron";
+
+export class IpcHandler implements IIpcHandler {
+
+ readonly channel: string;
+
+ readonly _handlerCallback: IpcHandlerCallback;
+
+ constructor(options: {
+ channel: string;
+ handlerCallback: IpcHandlerCallback;
+ }) {
+ this.channel = options.channel;
+ this._handlerCallback = options.handlerCallback;
+ }
+
+ handle() {
+ this.remove();
+ ipcMain.handle(this.channel, this._onInvoke);
+ }
+
+ remove() {
+ ipcMain.removeHandler(this.channel);
+ }
+
+ private _onInvoke = (_e: IpcMainInvokeEvent, payload?: string | null): Promise => {
+
+ return new Promise(async (resolve, reject) => {
+
+ console.log(`[IPC] Handle:${this.channel}`);
+
+ try {
+
+ const params = payload ? JSON.parse(payload) : null;
+
+ const res = await this._handlerCallback(params);
+
+ resolve(res as unknown as ReturnType);
+
+ } catch(e) {
+ console.log(`[IPC] Error:${this.channel}`);
+ reject(e);
+ }
+ });
+ }
+
+}
+
+
+export class IpcListener implements IIpcListener {
+
+ readonly channel: string;
+
+ readonly _listenerCallback: IpcListenerCallback;
+
+ constructor(options: {
+ channel: string;
+ listenerCallback: IpcListenerCallback;
+ }) {
+ this.channel = options.channel;
+ this._listenerCallback = options.listenerCallback;
+ }
+
+ listen() {
+ this.remove();
+ ipcMain.on(this.channel, this._onPost);
+ }
+
+ remove() {
+ ipcMain.removeAllListeners(this.channel);
+ }
+
+ private _onPost = (_e: IpcMainEvent, payload?: string | null): void => {
+
+ console.log(`[IPC] Post: ${this.channel}`);
+
+ const params = payload ? JSON.parse(payload) : null;
+ this._listenerCallback(params);
+ }
+
+}
diff --git a/src/composables/useMessageCanvas.ts b/src/composables/useMessageCanvas.ts
new file mode 100644
index 0000000..c85f7ed
--- /dev/null
+++ b/src/composables/useMessageCanvas.ts
@@ -0,0 +1,36 @@
+
+
+
+export default class Canvas {
+
+ messages: Message[];
+
+ /* seed canvas with messages on init */
+ constructor(messages: Message[]) {
+ this.messages = messages;
+ ipcEmit("seed-view", this.messages);
+ }
+
+ /* add a new message to the canvas */
+ add(message: Message) {
+ this.messages.push(message);
+ ipcEmit("update-view", message);
+ }
+
+ /* update an existing message */
+ update(message: Message) {
+
+ /* get the target message */
+ let target_message = this.messages.filter((m: Message) => {
+ return m.uid = message.uid;
+ })[0];
+
+ /* replace the target message */
+ if (target_message) {
+ target_message = message;
+ ipcEmit("update-view", message);
+ }
+
+ }
+
+}
diff --git a/src/composables/useSaveToJSON.ts b/src/composables/useSaveToJSON.ts
new file mode 100644
index 0000000..06ab4b9
--- /dev/null
+++ b/src/composables/useSaveToJSON.ts
@@ -0,0 +1,13 @@
+
+import {config} from "@/config";
+import fs from 'fs';
+
+export const saveToJson = (fileName: string, data: any) => {
+
+ fs.writeFile(config.configPath + fileName, JSON.stringify(data), (err) => {
+ if (err) {
+ console.log("Error when saving to json.")
+ }
+ })
+
+}
diff --git a/src/composables/useWebsockets.ts b/src/composables/useWebsockets.ts
new file mode 100644
index 0000000..d1cfc02
--- /dev/null
+++ b/src/composables/useWebsockets.ts
@@ -0,0 +1,89 @@
+
+"use strict";
+
+import WebSocket from 'ws';
+
+
+const _connectionCheckTimeout = 4000;
+const _reconnectTimeout = 1000;
+let _connectionCheckInterval: ReturnType;
+
+
+export default function useWebSockets(
+ messageCallback: (message: string) => void,
+ connectionStatusCallback: (alive: boolean) => void,
+) {
+
+ let socket: WebSocket;
+
+ const send = async (data: Record): Promise => {
+ return new Promise((resolve, reject) => {
+ if (socket) {
+ if (socket.readyState === WebSocket.OPEN) {
+ socket.send(JSON.stringify(data));
+ resolve(true);
+ }
+ }
+ reject(false);
+ });
+ }
+
+ const connect = (socketUrl: string, secret: string) => {
+
+ // avoid setting multiple interval;
+ if (_connectionCheckInterval) clearInterval(_connectionCheckInterval);
+
+ /* create a new socket */
+ socket = new WebSocket(socketUrl);
+
+ /* add event listeners */
+ socket.on("open", () => {
+
+ socket.send(JSON.stringify({key: secret}));
+
+ // ping server
+ _connectionCheckInterval = setInterval(() => {
+
+ socket.ping(null, true, (e: Error) => {
+ if (e) {
+ socket.close();
+ connectionStatusCallback(false);
+ setTimeout(() => connect(socketUrl, secret), _reconnectTimeout);
+ }
+ });
+
+ }, _connectionCheckTimeout);
+
+ });
+
+ socket.on("message", (event: WebSocket.MessageEvent) => {
+ console.log("message received", event);
+ messageCallback(event.toString())
+ });
+
+ socket.on("close", (event: WebSocket.CloseEvent) => {
+ connectionStatusCallback(false);
+ clearInterval(_connectionCheckInterval);
+ if (!event.wasClean) {
+ setTimeout(() => connect(socketUrl, secret), _reconnectTimeout);
+ }
+
+ });
+
+ socket.on("pong", () => connectionStatusCallback(true));
+
+ }
+
+ const close = () => {
+ if (socket) {
+ socket.close();
+ }
+ }
+
+ return {
+ connect,
+ send,
+ close
+ };
+
+}
diff --git a/src/config.js b/src/config.js
deleted file mode 100644
index bc47fb5..0000000
--- a/src/config.js
+++ /dev/null
@@ -1,11 +0,0 @@
-const { app } = require("electron");
-
-const DOMAIN = "crimata.com";
-
-const prod = !process.defaultApp;
-// const prod = true;
-
-module.exports = {
- PLATFORM: prod ? `https://app.${DOMAIN}` : `http://localhost:8760`,
- API: prod ? `https://${DOMAIN}/api` : `http://localhost:8761`
-}
diff --git a/src/config.ts b/src/config.ts
new file mode 100644
index 0000000..855b1b2
--- /dev/null
+++ b/src/config.ts
@@ -0,0 +1,17 @@
+
+import { app } from "electron";
+
+const env = process.env;
+
+const PLATFORM_PORT = env.PLATFORM_PORT || 8760;
+const PLATFORM_IP = env.PLATFORM_IP || 'http://127.0.0.1';
+
+const BUSINESS_PORT = env.BUSINESS_PORT || 3000;
+const BUSINESS_IP = env.BUSINESS_IP || 'http://127.0.0.1';
+
+export const config = {
+ PLATFORM_URL: `${PLATFORM_IP}:${PLATFORM_PORT}`,
+ BUSINESS_URL: `${BUSINESS_IP}:${BUSINESS_PORT}`,
+ BUSINESS_PREFIX: '/api',
+ configPath: app.getPath('userData')
+}
diff --git a/src/init.ts b/src/init.ts
new file mode 100644
index 0000000..9a0fdf6
--- /dev/null
+++ b/src/init.ts
@@ -0,0 +1,52 @@
+/**
+ * Entry point for Crimata electron app.
+ * "Look on my Works, ye Mighty, and despair!"
+ */
+
+"use strict";
+
+import { app, protocol } from "electron";
+import createWindow from "./window";
+import main from "./main";
+import { backgroundMitt } from '@/composables/useEmitter';
+
+console.log('Starting Crimata electron app.');
+
+// Scheme must be registered before the app is ready
+protocol.registerSchemesAsPrivileged([
+ { scheme: "app", privileges: { secure: true, standard: true } }
+]);
+
+const isDev = require('electron-is-dev');
+
+let win: boolean;
+
+// Listen for window creation.
+backgroundMitt.on('window-active', (state: boolean) => {
+ win = state;
+});
+
+/* Start main process on ready */
+app.on("ready", async () => {
+ await main();
+});
+
+// Must keep to ensure app doesn't quit on close.
+app.on("before-quit", async () => {
+});
+
+// Must keep to ensure app doesn't quit on close.
+app.on("window-all-closed", () => {
+});
+
+// When user clicks app icon (re-open)
+app.on("activate", () => {
+ if (!win) createWindow();
+});
+
+// Exit cleanly on request from parent process in development mode.
+if (isDev) {
+ process.on("SIGTERM", () => {
+ app.quit();
+ });
+}
diff --git a/src/io.js b/src/io.js
deleted file mode 100644
index f09d3cf..0000000
--- a/src/io.js
+++ /dev/null
@@ -1,69 +0,0 @@
-const WebSocket = require("ws");
-const { ipcMain } = require("electron");
-
-const config = require("./config");
-const { updateTray } = require("./tray");
-const { backgroundMitt, ipcEmit } = require("./utils/emitter");
-
-let connection = null;
-
-let pingId;
-let reconnectId;
-
-function connectToPlatform(account)
-{
- if (pingId) clearInterval(pingId);
-
- connection = new WebSocket(`${config.PLATFORM}/${account}`)
-
- .on("open", () => pingId = setInterval(() => connection.ping(null, true), 1000))
-
- .on("pong", () => setConnectionStatus(0))
-
- .on("error", () => {}) /** keep silent on error */
-
- .on("message", (payload) => backgroundMitt.emit("message", JSON.parse(payload)))
-
- .on("close", (_code, reason) => {
-
- setConnectionStatus(1);
-
- if (reason)
- {
- if (reason == "unauthorized")
- {
- backgroundMitt.emit("logout", reason);
- }
-
- return;
- }
-
- reconnectId = setTimeout(() => connectToPlatform(account), 500);
-
- });
-}
-
-function sendMessage(message)
-{
- if (connection.readyState === WebSocket.OPEN) {
- connection.send(JSON.stringify(message));
- } else console.log("Failed to send message");
-}
-
-function disconnectFromPlatform()
-{
- clearTimeout(reconnectId);
- if (connection.open) connection.close(1000, "logout");
-}
-
-function setConnectionStatus(status)
-{
- updateTray("disconnect", status);
- ipcEmit("ws", status);
-}
-
-ipcMain.on("message", (_e, message) => sendMessage(message));
-
-exports.connectToPlatform = connectToPlatform;
-exports.sendMessage = sendMessage;
-exports.disconnectFromPlatform = disconnectFromPlatform;
diff --git a/src/ipc/handlers.ts b/src/ipc/handlers.ts
new file mode 100644
index 0000000..5529662
--- /dev/null
+++ b/src/ipc/handlers.ts
@@ -0,0 +1,28 @@
+
+"use strict";
+
+import { accountLogin, accountLogout, accountProfile } from "@/account";
+import { IpcHandler } from "@/composables/useIpcMain";
+import { flush } from "@/audio";
+
+
+const LOGIN_CHANNEL = "invoke-account-login";
+const LOGOUT_CHANNEL = "invoke-account-logout";
+const GET_AUDIO_CHANNEL = "invoke-audio-flush";
+
+export const loginHandler = new IpcHandler({
+ channel: LOGIN_CHANNEL,
+ handlerCallback: accountLogin
+});
+
+export const logoutHandler = new IpcHandler({
+ channel: LOGOUT_CHANNEL,
+ handlerCallback: accountLogout
+});
+
+export const getAudioHandler = new IpcHandler({
+ channel: GET_AUDIO_CHANNEL,
+ handlerCallback: flush
+});
+
+
diff --git a/src/ipc/index.ts b/src/ipc/index.ts
new file mode 100644
index 0000000..e6eb90a
--- /dev/null
+++ b/src/ipc/index.ts
@@ -0,0 +1,33 @@
+
+"use strict";
+
+import * as handlers from "./handlers";
+import * as listeners from "./listeners";
+
+const ipcHandlers: IPCHandlers = {};
+const ipcListeners: IPCListeners = {};
+
+const _initHandlers = (): void => {
+ for (const [key, handler] of Object.entries(handlers)) {
+ if (!(key in ipcHandlers)) {
+ ipcHandlers[key] = handler;
+ handler.handle();
+ }
+ }
+};
+
+const _initListeners = (): void => {
+ for (const [key, listener] of Object.entries(listeners)) {
+ if (!(key in ipcListeners)) {
+ ipcListeners[key] = listener;
+ listener.listen();
+ }
+ }
+};
+
+export default function initIpcMain(): void {
+ _initHandlers();
+ _initListeners();
+}
+
+
diff --git a/src/ipc/listeners.ts b/src/ipc/listeners.ts
new file mode 100644
index 0000000..8ef5d45
--- /dev/null
+++ b/src/ipc/listeners.ts
@@ -0,0 +1,24 @@
+
+import { IpcListener } from "@/composables/useIpcMain"
+import { sendMessage } from '@/session';
+import { collect } from "@/audio";
+import { updateAppState } from "@/account";
+
+const CLIENT_MESSAGE_CHANNEL = "post-session-send"
+const GET_AUDIO_CHANNEL = "post-audio-collect";
+const APP_MOUNT_CHANNEL = "post-app-mount";
+
+export const messageListener = new IpcListener({
+ channel: CLIENT_MESSAGE_CHANNEL,
+ listenerCallback: sendMessage
+});
+
+export const audioChunkListener = new IpcListener({
+ channel: GET_AUDIO_CHANNEL,
+ listenerCallback: collect
+});
+
+export const appMountListener = new IpcListener({
+ channel: APP_MOUNT_CHANNEL,
+ listenerCallback: updateAppState
+});
diff --git a/src/main.js b/src/main.js
deleted file mode 100644
index 63e4d13..0000000
--- a/src/main.js
+++ /dev/null
@@ -1,28 +0,0 @@
-const { app, protocol, ipcMain, nativeTheme } = require("electron");
-
-const store = require("./utils/store");
-
-const { initTray } = require("./tray");
-const { createWin } = require("./window");
-const { initAccount } = require("./account");
-const { terminateAudio } = require("./audio");
-
-// Assert a light theme
-nativeTheme.themeSource = "light";
-
-app.on("ready", () =>
-{
- initTray();
-
- createWin();
-
- initAccount();
-});
-
-app.on("will-quit", terminateAudio);
-
-// When user clicks app icon (re-open)
-app.on("activate", createWin);
-
-// Prevents app from quitting on window close event
-app.on('window-all-closed', (e) => e.preventDefault());
diff --git a/src/main.ts b/src/main.ts
new file mode 100644
index 0000000..a360835
--- /dev/null
+++ b/src/main.ts
@@ -0,0 +1,39 @@
+/**
+ * Where the background logic really begins, gets called by app.onReady().
+ *
+ * Handles authentication. If profile is set, we launch a session, which consis
+ * of opening a connection with the platform, initializing the audio streams.
+ *
+ * The session is primarily an interface between the frontend and the platform,
+ * relaying messages from one to the other.
+ *
+ */
+
+import initIpcMain from "@/ipc/index";
+import { accountAuth, updateAppState } from "./account";
+import { launchSession } from "./session";
+import createWindow from "./window";
+
+let authState: AuthState | null;
+
+export default async function main() {
+
+ /* initiate controls for frontend to use when needed */
+ initIpcMain();
+
+ /* launch browser window */
+ await createWindow();
+
+ try {
+ authState = await accountAuth() as AuthState;
+ } catch(e) {
+ console.log('AUTH:', e);
+ authState = null;
+ } finally {
+ if (authState) {
+ launchSession(authState.token as string);
+ }
+ updateAppState();
+ }
+
+}
diff --git a/src/render/App.vue b/src/render/App.vue
new file mode 100644
index 0000000..686591b
--- /dev/null
+++ b/src/render/App.vue
@@ -0,0 +1,76 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/render/assets/connectLogo.svg b/src/render/assets/connectLogo.svg
deleted file mode 100644
index 7adceea..0000000
--- a/src/render/assets/connectLogo.svg
+++ /dev/null
@@ -1,12 +0,0 @@
-
diff --git a/src/render/assets/fonts/SF-Compact-Display-Bold.otf b/src/render/assets/fonts/SF-Compact-Display-Bold.otf
deleted file mode 100755
index 409e11a..0000000
Binary files a/src/render/assets/fonts/SF-Compact-Display-Bold.otf and /dev/null differ
diff --git a/src/render/assets/fonts/SF-Compact-Rounded-Bold.otf b/src/render/assets/fonts/SF-Compact-Rounded-Bold.otf
deleted file mode 100755
index 04cf9fc..0000000
Binary files a/src/render/assets/fonts/SF-Compact-Rounded-Bold.otf and /dev/null differ
diff --git a/src/render/assets/fonts/SF-Pro-Text-Regular.otf b/src/render/assets/fonts/SF-Pro-Text-Regular.otf
deleted file mode 100755
index 06dbe85..0000000
Binary files a/src/render/assets/fonts/SF-Pro-Text-Regular.otf and /dev/null differ
diff --git a/src/render/assets/settingsIcon.svg b/src/render/assets/settingsIcon.svg
deleted file mode 100644
index 25399f5..0000000
--- a/src/render/assets/settingsIcon.svg
+++ /dev/null
@@ -1,7 +0,0 @@
-
diff --git a/src/render/components/app.js b/src/render/components/app.js
deleted file mode 100644
index 1793563..0000000
--- a/src/render/components/app.js
+++ /dev/null
@@ -1,44 +0,0 @@
-import Login from "./login.js";
-import Splash from "./splash.js";
-import Header from "./header.js";
-import Messenger from "./messenger.js";
-
-const account = Vue.ref(null);
-
-const App =
-{
- components: {
- Splash,
- Header,
- Messenger,
- Login
- },
-
- setup()
- {
- return { account };
- },
-
- template: `
-
-
-
-
-
-
-
-
-
- `
-}
-
-window.mainApi.on("account", (value, reason) => {
- if (reason) alert(reason);
- account.value = value;
-});
-
-export default App;
-export { account };
-
-// Is called when window is about to close or reload
-window.onbeforeunload = () => console.log("beforeunload");
diff --git a/src/render/components/bubble.js b/src/render/components/bubble.js
deleted file mode 100644
index a42364c..0000000
--- a/src/render/components/bubble.js
+++ /dev/null
@@ -1,48 +0,0 @@
-const Bubble =
-{
- props: ["category", "text", "html", "blob", "modifier", "child"],
-
- setup(props)
- {
- const getUrl = () => {
- return `data:${props.category};base64,${props.blob}`;
- }
-
- Vue.onMounted(() => {
-
- const el = document.getElementById("messenger");
-
- setTimeout(() => {
-
- el.dispatchEvent(new CustomEvent('adjust-scroll'));
-
- }, 100);
-
- });
-
- if (!props.text) props.text = "...";
-
- return { getUrl };
- },
-
- template: `
-
-
-
-
-
{{ text }}
-
-
{{ text }}
-
-
-
-
![]()
-
-
{{ text }}
-
-
`
-};
-
-export default Bubble;
-
-// TODO: should include other styling/actions for audio bubbles
diff --git a/src/render/components/bubble.vue b/src/render/components/bubble.vue
new file mode 100644
index 0000000..982cef3
--- /dev/null
+++ b/src/render/components/bubble.vue
@@ -0,0 +1,331 @@
+
+
+
+
+
+
+
+
+
+ {{ context }}
+
+
+
+
+
+
+
+
+
+
diff --git a/src/render/components/context.js b/src/render/components/context.js
deleted file mode 100644
index 0c9e89c..0000000
--- a/src/render/components/context.js
+++ /dev/null
@@ -1,33 +0,0 @@
-
-const Context =
-{
- props: ["modifier", "context", "avatar", "id"],
-
- setup(props) {
- const playback = Vue.ref(false);
-
- Vue.onMounted(() => {
- window.mainApi.on("playback", (id, status) => {
- if (id === props.id) {
- playback.value = status;
- }
- });
- });
-
- return { playback };
- },
-
- template: `
-
-
-
-
- {{ context }}
-
- `
-}
-
-export default Context;
diff --git a/src/render/components/control/draggify.js b/src/render/components/control/draggify.js
deleted file mode 100644
index 23d2683..0000000
--- a/src/render/components/control/draggify.js
+++ /dev/null
@@ -1,195 +0,0 @@
-const saveLocation = "input_item_position";
-const defaultPosition = { x: 15, y: 400 };
-
-//---Dragabble Helper Funcs--------------------------------------
-
-// Calculate distance to nearest side.
-function calcSideProximity (elementX, elementLength, winW) {
-
- // Calc right short.
- let short = winW - elementX - elementLength;
-
- // See if it's left short.
- if (elementX + 20 < winW / 2) {
- short = elementX
- }
-
- return short
-}
-
-// Update elementX or elementY value on window resize
-function calcPosition (elementPosition, elementLength, percent, short, win) {
-
- // Is it close to the right/bottom side?
- if (percent > 0.75) {
- elementPosition = win - short - elementLength;
- }
-
- // Is it not close to a side?
- if (percent < 0.75 && percent > 0.25) {
- elementPosition = win * percent
- }
-
- return elementPosition
-}
-
-function draggify(elementId, parentId, margin) {
-
- let element;
- let parent;
-
- /* only compatible with elements having equal width and height */
- let elementLength;
-
- // Cords of inputItem.
- const elementX = Vue.ref();
- const elementY = Vue.ref();
-
- // Position of inputItem on terms of percentage of window.
- let percentX;
- let percentY;
-
- // How close inputItem is to closest X or Y side.
- let xShort;
- let yShort;
-
- //---Reposition Anime-----------------------------------------------
-
- // Move element to target smoothly.
- const repositionAnime = (xChange, yChange) => {
-
- const xStep = xChange / 1000;
- const yStep = yChange / 1000;
-
- for (let i = 1; i <= 1000; i++) {
-
- setTimeout(() => {
- elementX.value += xStep;
- elementY.value += yStep;
- }, 30) // 60fps
-
- }
- }
-
- //---Event Handlers-----------------------------------------------
-
- // Update the position of inputItem on mouse dragging.
- const onMouseMove = (e) => {
- e.preventDefault();
-
- elementX.value = element.offsetLeft + e.movementX;
- elementY.value = element.offsetTop + e.movementY;
- }
-
- // Add an event listener for dragging.
- const onMouseDown = (e) => {
- e.preventDefault();
- window.addEventListener('mousemove', onMouseMove);
- window.addEventListener('mouseup', onMouseUp);
- }
-
- // Update position references when user is done moving targetEl.
- const onMouseUp = (e) => {
- e.preventDefault();
-
- window.removeEventListener('mousemove', onMouseMove);
- window.removeEventListener('mouseup', onMouseUp);
-
- // See if and calculate reposition.
- let x = 0; // vector change
- let y = 0;
-
- const winW = parent.clientWidth;
- const winH = parent.clientHeight;
-
- if (elementX.value < 0) {
- x = (elementX.value - margin)*-1
- }
-
- if (elementX.value > winW - elementLength) {
- const b = winW - elementLength - margin;
- x = (elementX.value - b)*-1
- }
-
- // Reposition y
- if (elementY.value < 0) {
- y = (elementY.value - margin)*-1
- }
-
- if (elementY.value > winH - elementLength) {
- const d = winH - elementLength - margin;
- y = (elementY.value - d)*-1
- }
-
- // Reposition if needed.
- if (x !== 0 || y !== 0) repositionAnime(x, y);
-
- xShort = calcSideProximity(elementX.value, elementLength, winW);
- yShort = calcSideProximity(elementY.value, elementLength, winH);
-
- // Update percentages.
- percentX = elementX.value / winW;
- percentY = elementY.value / winH;
-
- // Save position.
- savePosition();
- }
-
- // Update position of targetEl on windowResize.
- const onWindowResize = (_e) => {
-
- elementX.value = calcPosition(elementX.value, elementLength, percentX, xShort, parent.clientWidth);
- elementY.value = calcPosition(elementY.value, elementLength, percentY, yShort, parent.clientHeight);
-
- savePosition();
- }
-
- const savePosition = () => {
- window.localStorage.setItem(saveLocation, JSON.stringify({
- x: elementX.value,
- y: elementY.value
- }));
- }
-
- //---------------------------------------------------------------
-
- Vue.onMounted(() => {
-
- element = document.getElementById(elementId);
- parent = document.getElementById(parentId);
-
- elementLength = element.offsetWidth;
-
- // Initialize the positional references.
- percentX = elementX.value / parent.clientWidth;
- percentY = elementY.value / parent.clientHeight;
-
- xShort = calcSideProximity(elementX.value, elementLength, parent.clientWidth);
- yShort = calcSideProximity(elementX.value, elementLength, parent.clientHeight);
-
- // Then, we can listen for window resize (and mousedown).
- element.addEventListener("mousedown", onMouseDown);
- parent.addEventListener('resize', onWindowResize)
- });
-
- // remove event listeners on component dismount.
- Vue.onUnmounted(() => {
- element.removeEventListener('mousedown', onMouseDown);
- parent.removeEventListener('resize', onWindowResize)
- parent.removeEventListener('mouseup', onMouseUp)
- parent.removeEventListener('mousemove', onMouseMove);
- });
-
- // Try loading initPosition, otherwise set default values
- let initPosition;
- const rawData = window.localStorage.getItem("saveLocation")
- rawData ? initPosition = JSON.parse(rawData) : initPosition = defaultPosition;
-
- elementX.value = initPosition.x;
- elementY.value = initPosition.y;
-
- return { elementX, elementY };
-
-}
-
-export default draggify;
diff --git a/src/render/components/control/drop.js b/src/render/components/control/drop.js
deleted file mode 100644
index ac9c790..0000000
--- a/src/render/components/control/drop.js
+++ /dev/null
@@ -1,24 +0,0 @@
-const onDrop = async (e) => {
- const file = e.dataTransfer.items[0].getAsFile();
-
- if (file.size >= 1 * 1000 * 1000) {
- alert("File must be under 1MB.");
- return;
- }
-
- const buffer = await file.arrayBuffer();
- const b64String = await window.mainApi.invoke("encode", buffer);
-
- let category = "file";
- if (file.type.startsWith("image/")) {
- category = "image"
- }
-
- window.mainApi.send("message", {
- category: category,
- text: file.name,
- blob: b64String
- });
-}
-
-export default onDrop;
diff --git a/src/render/components/control/scroll.js b/src/render/components/control/scroll.js
deleted file mode 100644
index 3e9a0bf..0000000
--- a/src/render/components/control/scroll.js
+++ /dev/null
@@ -1,20 +0,0 @@
-let el;
-
-const scroll = () => {
- if (!el) {
- el = document.getElementById("messenger");
- window.addEventListener('resize', scroll);
- }
- el.scrollTo({
- top: el.scrollHeight - el.clientHeight,
- behavior: 'smooth'
- });
-}
-
-export default scroll;
-
-// const isBottom = () => {
-// if (el) {
-// return el.scrollHeight - el.clientHeight <= el.scrollTop + 1;
-// }
-// }
\ No newline at end of file
diff --git a/src/render/components/control/text.js b/src/render/components/control/text.js
deleted file mode 100644
index cd18db9..0000000
--- a/src/render/components/control/text.js
+++ /dev/null
@@ -1,84 +0,0 @@
-const inputLength = 230;
-
-const metaKeys = [
- "Tab",
- "CapsLock",
- "Shift",
- "Control",
- "Alt",
- "Meta",
- " ",
- "ArrowLeft",
- "ArrowRight",
- "ArrowUp",
- "ArrowDown",
- "Enter",
- "Backspace",
- "Escape"
-];
-
-function useText(elementId, parentId, left)
-{
- let el;
- let parent;
-
- const show = Vue.ref(false);
- const leftSide = Vue.ref(false);
-
- const calcSide = () => {
- parent.clientWidth - left.value < inputLength ? leftSide.value = true : leftSide.value = false;
- }
-
- /* handle user typing */
- Vue.onMounted(() => {
-
- el = document.getElementById(elementId);
- parent = document.getElementById(parentId);
-
- window.addEventListener("keydown", (e) => {
-
- if (!show.value && !metaKeys.includes(e.key) && !e.ctrlKey)
- {
- show.value = true;
- }
- else if (show.value && ((el.value.length === 1 && e.key === "Backspace") || e.key === "Escape"))
- {
- show.value = false;
- }
-
- if (e.key === "Enter" && el.value)
- {
- if (el.value.length >= 250) {
- alert("250 character limit")
- return;
- }
-
- window.mainApi.send("message", {
- category: "text",
- text: el.value,
- blob: null
- });
-
- show.value = false;
- }
- });
-
- calcSide();
-
- });
-
- Vue.watch(left, calcSide);
-
- Vue.watch(show, (c, _p) => {
- if (c) {
- el.focus();
- } else {
- el.value = "";
- el.blur();
- }
- });
-
- return { leftSide, show };
-}
-
-export default useText;
diff --git a/src/render/components/controllers/bubble.control.ts b/src/render/components/controllers/bubble.control.ts
new file mode 100644
index 0000000..e69de29
diff --git a/src/render/components/controllers/helpers.ts b/src/render/components/controllers/helpers.ts
new file mode 100644
index 0000000..23976a8
--- /dev/null
+++ b/src/render/components/controllers/helpers.ts
@@ -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()
+}) {
+ return {
+ text: text,
+ audio: audio,
+ context: context,
+ uid: uid
+ };
+}
diff --git a/src/render/components/controllers/inputItem.control.audio.ts b/src/render/components/controllers/inputItem.control.audio.ts
new file mode 100644
index 0000000..facf30c
--- /dev/null
+++ b/src/render/components/controllers/inputItem.control.audio.ts
@@ -0,0 +1,77 @@
+import { onMounted, onUnmounted, ref, Ref } from "vue";
+import { postMessage } from "@/render/ipc";
+import { newMessage, animateAudioInput } from "./helpers";
+import { invokeReturnAudio, postAudioChunk } from "@/render/ipc";
+
+export default function useAudioInputController (typing: Ref) {
+
+ const recording = ref(false);
+ let mediaRecorder: MediaRecorder;
+
+ const { show, hide } = animateAudioInput();
+
+ // initialize audio
+ const conf = {audio: true, video: false}
+ navigator.mediaDevices.getUserMedia(conf).then((stream: MediaStream) => {
+
+ const options = {mimeType: 'audio/webm'};
+ mediaRecorder = new MediaRecorder(stream, options);
+
+ // post any mew audio to backend
+ mediaRecorder.addEventListener('dataavailable', (e: BlobEvent) => {
+ e.data.arrayBuffer().then((buff: ArrayBuffer) => {
+ postAudioChunk(buff);
+ });
+ });
+
+ // get audio and post new message to backend
+ mediaRecorder.addEventListener('stop', (_e: Event) => {
+ invokeReturnAudio().then((audio: ArrayBuffer[] | Error) => {
+ console.log(audio);
+ // postMessage(newMessage({audio: audio}));
+ });
+ });
+
+ });
+
+ // start recording on space bar
+ const record = () => {
+ console.log("INPT:Capturing audio...")
+ mediaRecorder.start();
+ recording.value = true;
+ show()
+ }
+
+ // stop recording and send on release
+ const stop = () => {
+ console.log("INPT:Stopping record.")
+ mediaRecorder.stop();
+ recording.value = false;
+ hide();
+ }
+
+ const onKeyDown = (e: KeyboardEvent) => {
+ if (e.keyCode == 32 && !typing.value) record();
+ }
+
+ const onKeyUp = (e: KeyboardEvent) => {
+ if (e.keyCode == 32 && recording.value) stop();
+ }
+
+ //-----------------------------------------------------------
+
+ onMounted(() => {
+ window.addEventListener("keydown", onKeyDown);
+ window.addEventListener("keyup", onKeyUp);
+ });
+
+ onUnmounted(() => {
+ window.removeEventListener("keydown", onKeyDown);
+ window.removeEventListener("keyup", onKeyUp);
+ })
+
+ return {
+ recording
+ }
+
+}
diff --git a/src/render/components/controllers/inputItem.control.text.ts b/src/render/components/controllers/inputItem.control.text.ts
new file mode 100644
index 0000000..941ae89
--- /dev/null
+++ b/src/render/components/controllers/inputItem.control.text.ts
@@ -0,0 +1,134 @@
+import { Ref, ref, watch, onMounted, onUnmounted } from "vue";
+import { postMessage } from "@/render/ipc";
+import { newMessage, animateTextInput } from "./helpers";
+
+
+export default function useTextInputController(elementX: Ref) {
+
+ let textInput: HTMLInputElement | null;
+
+ const { side, show, hide, switchSide } = animateTextInput();
+
+ let firstKey = true;
+ const typing = ref(false);
+
+ // Prep inputItem for typing.
+ const prepInput = () => {
+ show()
+ typing.value = true
+ }
+
+ // Clear and hide inputItem after done typing.
+ const clearInput = () => {
+
+ if (textInput) {
+ textInput.value = "";
+ textInput.blur();
+ }
+
+ hide()
+ firstKey = true;
+ typing.value = false;
+ }
+
+ // Send a message and clean up after.
+ const sendMessage = () => {
+ if (textInput) {
+
+ // Send it to the backend for processing.
+ const message = newMessage({
+ text: false
+ });
+
+ // postMessage(message);
+
+ clearInput()
+ }
+ }
+
+ // Keys that are capable of opening the text input (numbers and letters).
+ const isHotKey = (key: number) => {
+ if (key >= 47 && key <= 91) { // a letter
+ return true
+ }
+ }
+
+ //---Callbacks-----------------------------------------------
+
+ const onKeyDown = (e: KeyboardEvent) => {
+ const key = e.keyCode;
+
+ if (textInput) {
+
+ // Only runs on firstKey.
+ if (firstKey) {
+
+ if (!isHotKey(key)) {
+ return
+ }
+
+ prepInput()
+ }
+
+ textInput.focus();
+
+ // Close input when no text or on ESC.
+ if ((textInput.value == "") && (!firstKey) && (key === 8)) { // backspace
+ clearInput()
+ return
+ }
+
+ if (key === 27) { // escape
+ clearInput()
+ return
+ }
+
+ // Close and send on enter.
+ if (key === 13) {
+ if (textInput.value) {
+ sendMessage()
+ return
+ }
+ }
+
+ if (firstKey) firstKey = false
+ }
+ }
+
+ //-----------------------------------------------------------
+
+ // Watch parent position and update side.
+ watch(elementX, (elementX, _previous) => {
+ const winW = window.innerWidth
+
+ // Logic depends on the side we are on.
+ if (side.value === "right") {
+ if (winW - elementX < 230) {
+ switchSide()
+ side.value = "left"
+ }
+ }
+
+ else {
+ if (winW - elementX > 230) {
+ switchSide()
+ side.value = "right"
+ }
+ }
+
+ });
+
+ onMounted(() => {
+ textInput = document.getElementById("textInput") as HTMLInputElement;
+ window.addEventListener("keydown", onKeyDown);
+ })
+
+ onUnmounted(() => {
+ window.removeEventListener("keydown", onKeyDown);
+ });
+
+ return {
+ typing
+ }
+
+}
diff --git a/src/render/components/controllers/messenger.control.ts b/src/render/components/controllers/messenger.control.ts
new file mode 100644
index 0000000..b9c3a96
--- /dev/null
+++ b/src/render/components/controllers/messenger.control.ts
@@ -0,0 +1,38 @@
+import { ref } from 'vue';
+import useScroll from "@/render/composables/useScroll";
+
+const messagesRef = ref();
+
+/* seed the canvas with messages */
+const seedCanvas = (messages: Message[]) => {
+ messagesRef.value = messages;
+}
+
+const addMessage = (message: Message) => {
+ messagesRef.value.push(message);
+}
+
+const updateMessage = (message: Message) => {
+
+ let target_message = messagesRef.value.filter((m: Message) => {
+ return m.uid = message.uid;
+ })[0];
+
+ if (target_message) {
+ target_message = message;
+ }
+
+}
+
+export default function useMessages() {
+
+ const { updateScrollRef, adjustScroll } = useScroll("messenger");
+
+ return {
+ messagesRef,
+ seedCanvas,
+ addMessage,
+ updateMessage
+ };
+
+}
diff --git a/src/render/components/form.js b/src/render/components/form.js
deleted file mode 100644
index fbc7b0e..0000000
--- a/src/render/components/form.js
+++ /dev/null
@@ -1,83 +0,0 @@
-const debounce = (fn, delay) => {
-
- let timeoutId;
-
- const wrapper = (...args) => {
-
- if (timeoutId) {
- clearTimeout(timeoutId);
- }
-
- timeoutId = setTimeout(() => {
- fn(...args);
- }, delay);
-
- }
-
- return wrapper;
-}
-
-
-
-const SmartForm =
-{
- props: ["fields", "channel", "modifier"],
-
- setup(props)
- {
- let logsEl;
-
- const submit = debounce(async (data) => {
-
- let object = {};
- data.forEach(function(value, key){
- object[key] = value;
- });
-
- const err = await window.mainApi.invoke(props.channel, object)
-
- if (err) {
- logsEl.innerText = err;
- logsEl.style.color = "red";
- return;
- }
-
- logsEl.style.color = "green";
- logsEl.innerText = props.modifier;
-
- }, 2000);
-
- Vue.onMounted(() => {
-
- const el = document.getElementById("smart-form");
-
- logsEl = el.querySelector(".smart-form-logs");
-
- logsEl.innerText = props.modifier;
-
- el.addEventListener("input", () => {
- submit(new FormData(el))
- });
-
- });
- },
-
- template: `
- `
-}
-
-export default SmartForm;
diff --git a/src/render/components/header.js b/src/render/components/header.js
deleted file mode 100644
index 5aee0de..0000000
--- a/src/render/components/header.js
+++ /dev/null
@@ -1,23 +0,0 @@
-const Header =
-{
- setup() {
- const onNavBar = (command) => window.mainApi.send("nav", command);
- return { onNavBar };
- },
-
- template: `
-
-
- `
-}
-
-export default Header;
\ No newline at end of file
diff --git a/src/render/components/header.vue b/src/render/components/header.vue
new file mode 100644
index 0000000..63716c8
--- /dev/null
+++ b/src/render/components/header.vue
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+
+
diff --git a/src/render/components/input.js b/src/render/components/input.js
deleted file mode 100644
index b556dd0..0000000
--- a/src/render/components/input.js
+++ /dev/null
@@ -1,55 +0,0 @@
-import useText from "./control/text.js";
-import draggify from "./control/draggify.js";
-
-const Input =
-{
- props: ["person"],
-
- setup(props)
- {
- const initials = Vue.ref("");
-
- const recording = Vue.ref(false);
-
- const { elementX, elementY } = draggify("input", "app", 15);
-
- const { leftSide, show } = useText("input-text", "app", elementX);
-
- window.mainApi.on("record", (status) => recording.value = status);
-
- Vue.watch(() => props.person, (c, _p) => initials.value = getInitials(c));
-
- return { initials, elementX, elementY, recording, leftSide, show };
- },
-
- template: `
- `
-}
-
-function getInitials(name)
-{
- let rgx = new RegExp(/(\p{L}{1})\p{L}+/, 'gu');
-
- let initials = [...name.matchAll(rgx)] || [];
-
- return (
- (initials.shift()?.[1] || '') + (initials.pop()?.[1] || '')
- ).toUpperCase();
-}
-
-export default Input;
diff --git a/src/render/components/inputItem.vue b/src/render/components/inputItem.vue
new file mode 100644
index 0000000..edd844f
--- /dev/null
+++ b/src/render/components/inputItem.vue
@@ -0,0 +1,220 @@
+
+
+
+
+
+
+
diff --git a/src/render/components/login.js b/src/render/components/login.js
deleted file mode 100644
index f9a0f54..0000000
--- a/src/render/components/login.js
+++ /dev/null
@@ -1,40 +0,0 @@
-import SmartForm from "./form.js";
-
-const Login =
-{
- components: {
- SmartForm
- },
-
- setup()
- {
- return;
- },
-
- template: `
-
-
-
-
-
`
-}
-
-export default Login;
\ No newline at end of file
diff --git a/src/render/components/login.vue b/src/render/components/login.vue
new file mode 100644
index 0000000..bece6ea
--- /dev/null
+++ b/src/render/components/login.vue
@@ -0,0 +1,180 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/src/render/components/message.js b/src/render/components/message.js
deleted file mode 100644
index ce7231b..0000000
--- a/src/render/components/message.js
+++ /dev/null
@@ -1,62 +0,0 @@
-import Bubble from "./bubble.js";
-import Context from "./context.js";
-
-const Message =
-{
- components: {
- Bubble,
- Context
- },
-
- props: ["modifier", "content", "context", "avatar", "id", "seen"],
-
- setup(props)
- {
- return { calcChild };
- },
-
- template: `
-
-
-
-
-
-
-
`
-}
-
-function calcChild(index, len)
-{
- if (len === 1)
- {
- return "none-child";
- }
- else if (index === 0)
- {
- return "first-child";
- }
- else if (index === len - 1)
- {
- return "last-child";
- }
- else
- {
- return "middle-child";
- }
-}
-
-export default Message;
diff --git a/src/render/components/messenger.js b/src/render/components/messenger.js
deleted file mode 100644
index 572613d..0000000
--- a/src/render/components/messenger.js
+++ /dev/null
@@ -1,86 +0,0 @@
-import onDrop from "./control/drop.js";
-import scroll from "./control/scroll.js";
-
-import WS from "./ws.js";
-import Input from "./input.js";
-import Settings from "./settings.js";
-import Message from "./message.js";
-
-const Messenger = {
-
- components: {
- WS,
- Settings,
- Message,
- Input
- },
-
- setup()
- {
- const person = Vue.ref("");
- const messages = Vue.ref([]);
-
- let current = Vue.ref();
-
- Vue.onMounted(() => {
-
- window.mainApi.on("message", (update) => {
-
- if (update.person)
- {
- person.value = update.person;
- messages.value = update.messages;
- current.value = messages.value.at(-1);
- }
- else if (update.name)
- {
- person.value = update.name;
- }
- else if (update.modifier)
- {
- messages.value.push(update);
- current.value = update;
- }
- else if (update.category)
- {
- current.value.content.push(update);
- }
- else if (update.context)
- {
- current.value.context = update.context;
- }
- else
- {
- current.value.content.at(-1).text = update.text;
- }
-
- setTimeout(scroll, 10);
-
- });
-
- window.mainApi.send("messenger");
- });
-
- return { person, messages, onDrop };
- },
-
- template: `
-
-
-
-
-
-
-
-
-
`
-}
-
-export default Messenger;
diff --git a/src/render/components/messenger.vue b/src/render/components/messenger.vue
new file mode 100644
index 0000000..ee56549
--- /dev/null
+++ b/src/render/components/messenger.vue
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/render/components/settings.js b/src/render/components/settings.js
deleted file mode 100644
index 8bcb994..0000000
--- a/src/render/components/settings.js
+++ /dev/null
@@ -1,40 +0,0 @@
-
-const Settings =
-{
- setup() {
- const active = Vue.ref(false);
-
- function hideSettings(e) {
- if (e.key == "Escape") {
- active.value = false;
- window.removeEventListener("keydown", hideSettings);
- }
- }
-
- function showSettings() {
- active.value = true;
- window.addEventListener("keydown", hideSettings);
- }
-
- const logout = async () => {
- await window.mainApi.send("logout");
- }
-
- return { showSettings, active, logout, window };
- },
-
- template: `
-
-
-
-
-
-
- `
-}
-
-export default Settings;
\ No newline at end of file
diff --git a/src/render/components/settings.vue b/src/render/components/settings.vue
new file mode 100644
index 0000000..cf881c3
--- /dev/null
+++ b/src/render/components/settings.vue
@@ -0,0 +1,164 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/render/components/splash.js b/src/render/components/splash.js
deleted file mode 100644
index 220eaad..0000000
--- a/src/render/components/splash.js
+++ /dev/null
@@ -1,50 +0,0 @@
-const Splash =
-{
- template: `
-
-
-
- `
-}
-
-export default Splash;
\ No newline at end of file
diff --git a/src/render/components/splash.vue b/src/render/components/splash.vue
new file mode 100644
index 0000000..247847a
--- /dev/null
+++ b/src/render/components/splash.vue
@@ -0,0 +1,69 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/render/components/ws.js b/src/render/components/ws.js
deleted file mode 100644
index 265915b..0000000
--- a/src/render/components/ws.js
+++ /dev/null
@@ -1,35 +0,0 @@
-
-const WS =
-{
- setup()
- {
- const status = Vue.ref();
-
- Vue.onMounted(() => {
-
- window.mainApi.on("ws", (val) => {
- status.value = val;
- });
-
- });
-
- return { status, window }
- },
-
- template: `
-
-
-
![]()
-
-
-
-
`
-}
-
-export default WS;
\ No newline at end of file
diff --git a/src/render/composables/useDraggify.ts b/src/render/composables/useDraggify.ts
new file mode 100644
index 0000000..f3d8978
--- /dev/null
+++ b/src/render/composables/useDraggify.ts
@@ -0,0 +1,199 @@
+import { onMounted, ref, onUnmounted } from "vue";
+
+//---Dragabble Helper Funcs--------------------------------------
+
+// Calculate distance to nearest side.
+function calcSideProximity (elementX: number, winW: number) {
+
+ // Calc right short.
+ let short = winW - elementX - 40;
+
+ // See if it's left short.
+ if (elementX + 20 < winW / 2) {
+ short = elementX
+ }
+
+ return short
+}
+
+// Update elementX or elementY value on window resize
+function calcPosition (elementPosition: number, percent: number,
+ short: number, win: number) {
+
+ // Is it close to the right/bottom side?
+ if (percent > 0.75) {
+ elementPosition = win - short - 40;
+ }
+
+ // Is it not close to a side?
+ if (percent < 0.75 && percent > 0.25) {
+ elementPosition = win * percent
+ }
+
+ return elementPosition
+}
+
+export default function draggify(elementId: string, xStart: number,
+ yStart: number, margin: number) {
+
+ let element: HTMLElement | null;
+
+ // Cords of inputItem.
+ const elementX = ref(0);
+ const elementY = ref(0);
+
+ // Position of inputItem on terms of percentage of window.
+ let percentX: number;
+ let percentY: number;
+
+ // How close inputItem is to closest X or Y side.
+ let xShort: number;
+ let yShort: number;
+
+ // Keep track of window size;
+ let winW = window.innerWidth;
+ let winH = window.innerHeight;
+
+ //---Reposition Anime-----------------------------------------------
+
+ // Move element to target smoothly.
+ const repositionAnime = (xChange: number, yChange: number) => {
+ const xStep = xChange / 6000;
+ const yStep = yChange / 6000;
+
+ for (let i = 1; i <= 6000; i++) {
+
+ setTimeout(() => {
+ elementX.value += xStep;
+ elementY.value += yStep;
+ }, 16) // 60fps
+
+ }
+ }
+
+ //---Event Handlers-----------------------------------------------
+
+ // Update the position of inputItem on mouse dragging.
+ const onMouseMove = (e: any) => {
+ e.preventDefault()
+ element = document.getElementById(elementId);
+
+ if (element) {
+ const inputItemRect = element.getBoundingClientRect();
+ elementX.value = inputItemRect.left + e.movementX;
+ elementY.value = inputItemRect.top + e.movementY;
+ }
+
+ }
+
+ // Add an event listener for dragging.
+ const onMouseDown = (_e: any) => {
+ _e.preventDefault()
+ window.addEventListener('mousemove', onMouseMove, true);
+ }
+
+ // Update position references when user is done moving targetEl.
+ const onMouseUp = (_e: any) => {
+ window.removeEventListener('mousemove', onMouseMove, true);
+
+ // See if and calculate reposition.
+ let x = 0; // vector change
+ let y = 0;
+
+ if (elementX.value < 0) {
+ x = (elementX.value - margin)*-1
+ }
+
+ if (elementX.value > winW - 40) {
+ const b = winW - 40 - margin;
+ x = (elementX.value - b)*-1
+ }
+
+ // Reposition y
+ if (elementY.value < 0) {
+ y = (elementY.value - margin)*-1
+ }
+
+ if (elementY.value > winH - 40) {
+ const d = winH - 40 - margin;
+ y = (elementY.value - d)*-1
+ }
+
+ // Reposition if needed.
+ if (x !== 0 || y !== 0) repositionAnime(x, y);
+
+ xShort = calcSideProximity(elementX.value, winW);
+ yShort = calcSideProximity(elementY.value, winH);
+
+ // Update percentages.
+ percentX = elementX.value / winW;
+ percentY = elementY.value / winH;
+
+ // Save position.
+ const position = {
+ x: elementX.value,
+ y: elementY.value
+ }
+ window.localStorage.setItem("inputItem_position", JSON.stringify(position));
+ }
+
+ // Update position of targetEl on windowResize.
+ const onWindowResize = (_e: any) => {
+
+ // Update window dimensions.
+ winW = window.innerWidth;
+ winH = window.innerHeight;
+
+ elementX.value = calcPosition(elementX.value, percentX, xShort, winW);
+ elementY.value = calcPosition(elementY.value, percentY, yShort, winH);
+
+ }
+
+ //---------------------------------------------------------------
+
+ onMounted(() => {
+ element = document.getElementById(elementId);
+
+ if (element) {
+ element.addEventListener('mousedown', onMouseDown, false);
+ }
+
+ window.addEventListener("mouseup", onMouseUp, false);
+
+ // Initialize the positional references.
+ percentX = elementX.value / window.innerWidth;
+ percentY = elementY.value / window.innerHeight;
+
+ xShort = calcSideProximity(elementX.value, window.innerWidth);
+ yShort = calcSideProximity(elementX.value, window.innerHeight);
+
+ // Then, we can listen for window resize.
+ window.addEventListener('resize', onWindowResize, false);
+ });
+
+ // Try loading initPosition, otherwise set default values
+ let initPosition: any;
+ const rawData = window.localStorage.getItem("inputItem_position")
+
+ if (rawData) {
+ initPosition = JSON.parse(rawData)
+ } else {
+ initPosition = {x: xStart, y: yStart}
+ }
+
+ elementX.value = initPosition.x;
+ elementY.value = initPosition.y;
+
+ // remove event listeners on component dismount.
+ onUnmounted(() => {
+ window.removeEventListener('resize', onWindowResize);
+ window.removeEventListener('mouseup', onMouseUp);
+ if (element) element.removeEventListener('mousedown', onMouseDown);
+ })
+
+ return {
+ elementX,
+ elementY
+ }
+
+}
diff --git a/src/render/composables/useIpcRend.ts b/src/render/composables/useIpcRend.ts
new file mode 100644
index 0000000..9ba7f73
--- /dev/null
+++ b/src/render/composables/useIpcRend.ts
@@ -0,0 +1,54 @@
+
+import { IpcRendererEvent } from "electron";
+
+export class IpcRendererListener implements IIpcListener {
+
+ readonly channel: string;
+
+ readonly _listenerCallback: IpcListenerCallback;
+
+ constructor(options: {
+ channel: string;
+ listenerCallback: IpcListenerCallback;
+ }) {
+ this.channel = options.channel;
+ this._listenerCallback = options.listenerCallback;
+ }
+
+ listen() {
+ this.remove();
+ window.ipcRenderer.on(this.channel, this._onPost);
+ }
+
+ remove() {
+ window.ipcRenderer.removeAllListeners(this.channel);
+ }
+
+ private _onPost = (_e: IpcRendererEvent, payload: InputParam): void => {
+ console.log(`[IPC] Post: ${this.channel}`);
+ this._listenerCallback(payload);
+ }
+
+}
+
+
+export default function useIpcRenderer () {
+
+ const invoke = async (endpoint: string, payload: any) => {
+ try {
+ const res = await window.ipcRenderer.invoke(endpoint, payload);
+ return res;
+ } catch (e) {
+ throw e;
+ }
+ }
+
+ const post = (endpoint: string, payload: any) => {
+ window.ipcRenderer.send(endpoint, payload);
+ };
+
+ return {
+ invoke,
+ post,
+ }
+}
diff --git a/src/render/composables/useMessages.ts b/src/render/composables/useMessages.ts
new file mode 100644
index 0000000..f731584
--- /dev/null
+++ b/src/render/composables/useMessages.ts
@@ -0,0 +1,33 @@
+// shared
+import { ref, Ref } from "vue";
+import useScroll from "@/render/composables/useScroll";
+
+export const messages: Ref> = ref([]);
+
+export const setMessages: IpcListenerCallback> = (payload) => {
+ messages.value = payload as Array;
+}
+
+export const addMessage: IpcListenerCallback = (payload) => {
+ messages.value.push(payload as Message);
+}
+
+export const updateMessage: IpcListenerCallback = (payload) => {
+ const message = payload as Message;
+
+ let targetMessage = messages.value.filter((m: Message) => {
+ return m.uid = message.uid;
+ })[0];
+
+ if (targetMessage) {
+ targetMessage = message;
+ }
+
+}
+
+export default {
+ messages,
+ setMessages,
+ addMessage,
+ updateMessage
+};
diff --git a/src/render/composables/useProfile.ts b/src/render/composables/useProfile.ts
new file mode 100644
index 0000000..3575de7
--- /dev/null
+++ b/src/render/composables/useProfile.ts
@@ -0,0 +1,27 @@
+// shared
+import { ref } from "vue";
+
+export const profile = ref();
+
+export const authComplete = ref(false);
+
+export const setProfile: IpcListenerCallback = (payload) => {
+ payload ? profile.value = payload : clearProfile();
+ showRender();
+};
+
+export const clearProfile = () => {
+ profile.value = null;
+};
+
+export const showRender = () => {
+ authComplete.value = true;
+};
+
+export default {
+ setProfile,
+ clearProfile,
+ profile,
+ showRender,
+ authComplete,
+};
diff --git a/src/render/composables/useScroll.ts b/src/render/composables/useScroll.ts
new file mode 100644
index 0000000..811e7fc
--- /dev/null
+++ b/src/render/composables/useScroll.ts
@@ -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,
+ };
+
+}
\ No newline at end of file
diff --git a/src/render/index.html b/src/render/index.html
deleted file mode 100644
index e45bb6c..0000000
--- a/src/render/index.html
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/src/render/index.js b/src/render/index.js
deleted file mode 100644
index d7df8b0..0000000
--- a/src/render/index.js
+++ /dev/null
@@ -1,5 +0,0 @@
-import App from "./components/app.js";
-
-window.path = "./";
-
-Vue.createApp(App).mount("#app");
diff --git a/src/render/ipc.ts b/src/render/ipc.ts
new file mode 100644
index 0000000..bc68049
--- /dev/null
+++ b/src/render/ipc.ts
@@ -0,0 +1,80 @@
+
+import useIpc from "@/render/composables/useIpcRend";
+import * as rendererListeners from "./listeners";
+
+const { post, invoke } = useIpc();
+
+/**
+ *
+ * Account and auth related endpoints
+ *
+ */
+
+
+export const invokeLogin = async (
+ payload: LoginPayload
+): Promise => (
+ await invoke('invoke-account-login', JSON.stringify(payload))
+);
+
+export const invokeLogout = async (): Promise => (
+ await invoke("invoke-account-logout", null)
+);
+
+/**
+ *
+ * Audio endpoints
+ *
+ */
+
+export const postAudioChunk = (chunk: ArrayBuffer): void => (
+ post("post-audio-collect", chunk)
+);
+
+export const invokeReturnAudio = async (): Promise => (
+ await invoke("invoke-audio-flush", null)
+);
+
+/**
+ *
+ * Crimata Platform (session) endpoints
+ *
+ */
+
+export const invokeSession = async (cid: string): Promise => (
+ await invoke("messenger-init", cid)
+);
+
+export const postMessage = (payload: Message): void => (
+ post('post-session-send', payload)
+);
+
+export const postAppMount = (): void => (
+ post('post-app-mount', null)
+);
+
+
+/**
+ *
+ * Ipc Renderer Listeners
+ *
+ */
+
+let ipcListeners: IPCListeners = {};
+
+export const initIpcRendererListeners = () => {
+ for (const [key, listener] of Object.entries(rendererListeners)) {
+ if (!(key in ipcListeners)) {
+ ipcListeners[key] = listener;
+ listener.listen();
+ }
+ }
+};
+
+export const removeListeners = () => {
+ for (const [key, listener] of Object.entries(rendererListeners)) {
+ listener.remove();
+ }
+ ipcListeners = {};
+};
+
diff --git a/src/render/listeners.ts b/src/render/listeners.ts
new file mode 100644
index 0000000..876d206
--- /dev/null
+++ b/src/render/listeners.ts
@@ -0,0 +1,32 @@
+
+import { IpcRendererListener } from "./composables/useIpcRend"
+import { setProfile } from "./composables/useProfile";
+import { setMessages, addMessage, updateMessage } from "./composables/useMessages";
+
+const SET_PROFILE_CHANNEL = "set-profile";
+
+const INIT_MESSAGES_CHANNEL = "init-messages";
+const ADD_MESSAGE_CHANNEL = "add-message";
+const UPDATE_MESSAGE_CHANNEL = "update-message";
+
+export const setProfileListener = new IpcRendererListener({
+ channel: SET_PROFILE_CHANNEL,
+ listenerCallback: setProfile
+});
+
+export const initMessagesListener = new IpcRendererListener({
+ channel: INIT_MESSAGES_CHANNEL,
+ listenerCallback: setMessages
+});
+
+
+export const addMessagesListener = new IpcRendererListener({
+ channel: ADD_MESSAGE_CHANNEL,
+ listenerCallback: addMessage
+});
+
+export const updateMessagesListener = new IpcRendererListener({
+ channel: UPDATE_MESSAGE_CHANNEL,
+ listenerCallback: updateMessage
+});
+
diff --git a/src/render/main.css b/src/render/main.css
deleted file mode 100644
index cd26154..0000000
--- a/src/render/main.css
+++ /dev/null
@@ -1,582 +0,0 @@
-
-@font-face {
- font-family: "Default";
- src: url("assets/fonts/SF-Pro-Text-Regular.otf");
-}
-
-@font-face {
- font-family: "Compact";
- src: url("assets/fonts/SF-Compact-Display-Bold.otf");
-}
-
-@font-face {
- font-family: "Rounded";
- src: url("assets/fonts/SF-Compact-Rounded-Bold.otf");
-}
-
-html, body {
- margin: 0;
- padding: 0;
- /*background-color: rgba(235, 235, 235, 0.75);*/
-}
-
-/* The first word of the class is the component that it modifys. */
-
-#splash {
- width: 100%;
- height: 100%;
- display: flex;
- align-items: center;
- justify-content: center;
-}
-
-#app {
- position: relative; /* must explicitly be declared */
- font-family: "Default";
- -webkit-font-smoothing: antialiased;
- height: 100vh; /* 100% for website */
- width: 100vw; /* 100% for website */
- border-radius: 15px;
-}
-
-#header-titlebar {
- position: absolute;
- width: 100%;
- height: 54px;
- opacity: 0.75;
- background-color: #EBEBEB;
- -webkit-app-region: drag;
- border: none;
- outline: none;
- z-index: 1;
- border-top-left-radius: 15px;
- border-top-right-radius: 15px;
-}
-
-.contacts {
- margin: 0;
- padding: 0;
- border-radius: inherit;
- background-color: #DBDBDB;
-}
-
-.contacts > li {
- padding: 12px;
- display: flex;
-}
-
-.contacts img {
- height: 30px;
- border-radius: 50%;
-}
-
-.contacts .info {
- margin-left: 10px;
- margin-right: 10px;
-}
-
-.contacts .name {
-
-}
-
-.contacts .email {
- font-family: "Compact";
- font-size: 12px;
- color: #898989;
- overflow: w;
- word-wrap: break-word;
-}
-
-#header-menu {
- position: absolute;
- margin-left: 20px;
- margin-top: 20px;
- z-index: 2;
-}
-
-.header-menu-button {
- min-width: 14px;
- min-height: 14px;
- border-radius: 50%;
-}
-
-.header-exit-button {
- background-color: #FF6157;
-}
-
-.header-exit-button:active {
- background: #c14645;
-}
-
-.header-min-button {
- background-color: #FFC12F;
- margin-left: 8px;
-}
-
-.header-min-button:active {
- background-color: #c08e38;
-}
-
-#login {
- width: 100vw;
- height: 100vh;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
-}
-
-#messenger {
- width: 100%;
- height: 100%;
- overflow: auto;
-}
-
-/* hide native scrollbar */
-#messenger::-webkit-scrollbar {
- display: none;
-}
-
-#ws {
- position: absolute;
- width: 100%;
- height: 54px;
- display: flex;
- align-items: center;
- justify-content: center;
- z-index: 1;
-}
-
-#ws-logo {
- transition: all 0.5s;
-}
-
-#ws-logo.move {
- transform: translateX(15px);
-}
-
-#ws-dots {
- opacity: 0;
- transition: all 0.5s;
-}
-
-#ws-dots.move {
- opacity: 1;
- transform: translateX(-15px);
-}
-
-.ws-dot {
- width: 6px;
- height: 6px;
- border-radius: 50%;
- background-color: #48E065;
- position: relative;
- transform: translateX(-15px);
- animation: ws-dot-flashing 1s infinite linear alternate;
- animation-delay: .25s;
-}
-
-.ws-dot::before, .ws-dot::after {
- content: '';
- display: inline-block;
- position: absolute;
- box-shadow: 0px 0px 10px 0px rgba(0,0,0,0.15);
-}
-
-.ws-dot::before {
- width: 6px;
- height: 6px;
- border-radius: 50%;
- background-color: #48E065;
- left: -9px;
- animation: ws-dot-flashing 1s infinite alternate;
- animation-delay: 0s;
-}
-
-.ws-dot::after {
- width: 6px;
- height: 6px;
- border-radius: 50%;
- background-color: #48E065;
- left: 9px;
- animation: ws-dot-flashing 1s infinite alternate;
- animation-delay: 0.5s;
-}
-
-@keyframes ws-dot-flashing {
- 0% {
- background-color: #48E065;
- }
- 50%,
- 100% {
- background-color: #9B9B9B;
- }
-}
-
-#input {
- position: absolute;
- display: flex;
- align-items: center;
- justify-content: center;
- padding: 0;
- width: 40px;
- height: 40px;
- z-index: 3;
- /* To prevent window drag when overlapping with titlebar. */
- -webkit-app-region: no-drag;
- border-radius: 50%;
- /* Set opacity here to not affect child. */
- background-color: rgba(235, 235, 235, 0.75);
- cursor: pointer;
- box-shadow: 0px 0px 10px 0px rgba(0,0,0,0.15);
-}
-
-#input-text {
- position: absolute;
- min-width: 150px;
- height: 16px;
- border-radius: 18px;
- padding: 10px;
- outline: none;
- border: none;
- font-size: 14px;
- pointer-events: none;
- background-color: white;
- z-index: -1;
- box-shadow: 0px 0px 10px 0px rgba(0,0,0,0.15);
- transform-origin: center;
- opacity: 0;
- transform: translateX(70%);
- transition: all 0.5s;
-}
-
-#input-text.show {
- opacity: 1;
-}
-
-#input-text.leftSide {
- transform: translateX(-70%);
-}
-
-#settings {
- position: absolute;
- width: 100%;
- height: 100%;
- background-color: rgba(235, 235, 235, 0.75);
- z-index: 4;
- display: flex;
- flex-direction: column;
- justify-content: center;
- align-items: center;
- animation-name: settings-appear;
- animation-duration: 0.5s;
- border-radius: inherit;
-}
-
-@keyframes settings-appear {
- from {
- background-color: rgba(235, 235, 235, 0);
- }
- to {
- background-color: rgba(235, 235, 235, 0.75);
- }
-}
-
-.settings-icon {
- position: absolute;
- right: 0;
- border: none;
- outline: none;
- display: flex;
- flex-direction: row;
- padding: 5px;
- margin-right: 20px;
- margin-top: 19px;
- z-index: 2;
- background-color: Transparent;
-}
-
-.settings-icon:hover {
- cursor: pointer;
-}
-
-.settings-account {
- font-size: 12px;
- font-weight: bold;
- margin-bottom: 25px;
-}
-
-.settings-logout-button {
- font-family: "Compact";
- background-color: #B7B7B7;
- padding: 10px 20px;
- border-radius: 20px;
- font-size: 14px;
-}
-
-.settings-logout-button:hover {
- cursor: pointer;
-}
-
-.settings-version {
- position: absolute;
- left: 50%;
- top: 75%;
- transform: translate(-50%, -50%);
- font-size: 12px;
- font-weight: bold;
- color: #575757;
-}
-
-.message {
- width: inherit;
- display: flex;
- flex-direction: column;
- padding-top: 9px;
- padding-bottom: 9px;
- animation-name: message-init-anim;
- animation-duration: 0.25s;
-}
-
-@keyframes message-init-anim {
- from {
- opacity: 0;
- } to {
- opacity: 1;
- }
-}
-
-.message:first-child {
- margin-top: 55px;
-}
-
-.message:last-child {
- margin-bottom: 6px;
-}
-
-.message-session {
- width: 100vw;
- display: flex;
- justify-content: center;
- align-items: center;
- font-family: "Compact";
- font-size: 12px;
- color: #9B9B9B;
- margin-bottom: 18px;
-}
-
-.client-message {
- align-items: flex-end;
-}
-
-.ai-message {
- align-items: flex-start;
-}
-
-.admin-message {
- align-items: center;
-}
-
-.bubble {
- position: relative;
- max-width: 66%;
- font-size: 14px;
- border-radius: 18px;
- margin-bottom: 4px;
- overflow-wrap: break-word;
-}
-
-.bubble-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: bubble-notify-anim;
- animation-duration: 5s;
-}
-
-@keyframes bubble-notify-anim {
- 0%, 90% {
- transform: scale(1);
- }
- 100% {
- transform: scale(0);
- }
-}
-
-.ai-bubble {
- background-color: #FFFFFF;
- margin-left: 15px;
-}
-
-.client-bubble {
- color: white;
- background-color: #58C4FD;
- margin-right: 15px;
-}
-
-.ai-first-child {
- border-bottom-left-radius: 9px;
-}
-
-.ai-middle-child {
- border-top-left-radius: 9px;
- border-bottom-left-radius: 9px;
-}
-
-.ai-last-child {
- border-top-left-radius: 9px;
-}
-
-.client-first-child {
- border-bottom-right-radius: 9px;
-}
-
-.client-middle-child {
- border-top-right-radius: 9px;
- border-bottom-right-radius: 9px;
-}
-
-.client-last-child {
- border-top-right-radius: 9px;
-}
-
-.bubble > p {
- margin: 0px;
- padding: 10px;
-}
-
-.bubble > div {
- border-radius: inherit;
-}
-
-.bubble > img {
- border-radius: inherit;
- display: block;
- max-width: 100%;
-}
-
-.bubble a {
- display: inline-block;
- font-family: "Rounded";
- text-decoration: none;
- border-radius: inherit;
- background-color: #D9D9D9;
- padding: 10px;
- color: #727272;
-}
-
-.context {
- position: relative;
- min-height: 14px;
- display: flex;
- align-items: center;
- font-family: "Compact";
- font-size: 12px;
- margin-top: 5px;
-}
-
-.ai-context {
- margin-left: 15px;
-}
-
-.client-context {
- margin-right: 15px;
-}
-
-.context-avatar {
- margin-right: 5px;
- border-radius: 50%;
- height: 30px;
-}
-
-/* ---------- shared between components ---------- */
-
-.button {
- border: none;
- outline: none;
- text-decoration: none;
-}
-
-/*.button:hover {
- cursor: pointer;
-}*/
-
-/* shake a div to signal error*/
-.shake {
- animation: shake 0.82s cubic-bezier(.36,.07,.19,.97) both;
- transform: translate3d(0, 0, 0);
- backface-visibility: hidden;
- perspective: 1000px;
-}
-
-@keyframes shake {
- 10%, 90% {
- transform: translate3d(-1px, 0, 0);
- }
-
- 20%, 80% {
- transform: translate3d(2px, 0, 0);
- }
-
- 30%, 50%, 70% {
- transform: translate3d(-4px, 0, 0);
- }
-
- 40%, 60% {
- transform: translate3d(4px, 0, 0);
- }
-}
-
-.audio {
- animation-name: audio-anim;
- animation-duration: 2s;
- animation-iteration-count: infinite;
-}
-
-@keyframes audio-anim {
- 0%,
- 100% {
- box-shadow: 0 0 0 0.4em rgba(195, 195, 195, 0.4), 0 0 0 0.25em rgba(195, 195, 195, 0.15);
- }
- 25% {
- box-shadow: 0 0 0 0.15em rgba(195, 195, 195, 0.15), 0 0 0 0.4em rgba(195, 195, 195, 0.3);
- }
- 50% {
- box-shadow: 0 0 0 0.55em rgba(195, 195, 195, 0.55), 0 0 0 0.15em rgba(195, 195, 195, 0.05);
- }
- 75% {
- box-shadow: 0 0 0 0.25em rgba(195, 195, 195, 0.25), 0 0 0 0.55em rgba(195, 195, 195, 0.45);
- }
-}
-
-.smart-form {
- width: 225px;
- display: flex;
- flex-direction: column;
- justify-content: flex-start;
-}
-
-.text-field {
- width: 100%;
- padding: 16px;
- background: none;
- border: none;
- margin: 4px;
- font-size: 14px;
- border-radius: 10px;
- background-color: #d1d1d1;
- outline: none;
-}
-
-.smart-form-logs {
- font-family: "Compact";
- font-size: 12px;
- text-align: left;
- margin-left: 10px;
-}
diff --git a/src/render/main.ts b/src/render/main.ts
new file mode 100644
index 0000000..5375889
--- /dev/null
+++ b/src/render/main.ts
@@ -0,0 +1,21 @@
+
+// src/main.ts
+
+import App from "./App.vue";
+
+import mitt from "mitt";
+import { createApp } from "vue";
+
+import { initIpcRendererListeners } from "./ipc"
+
+
+// Handle ipcMain events.
+initIpcRendererListeners();
+
+// Handle events.
+const emitter = mitt();
+
+const app = createApp(App);
+
+app.provide("mitt", emitter);
+app.mount("#app");
diff --git a/src/render/preload.js b/src/render/preload.js
deleted file mode 100644
index adef673..0000000
--- a/src/render/preload.js
+++ /dev/null
@@ -1,53 +0,0 @@
-const { contextBridge, ipcRenderer } = require("electron");
-
-/** The main <-> render interface */
-
-const validFromMainChannels = [
- "playback",
- "account",
- "message",
- "record",
- "ws"
-];
-
-const validToMain = [
- "messenger",
- "message",
- "logout",
- "encode",
- "auth",
- "nav"
-]
-
-ipcRenderer.setMaxListeners(250);
-
-// Expose protected methods that allow the renderer process to use
-// the ipcRenderer without exposing the entire object
-// TODO: Implement argument filtering for added security
-contextBridge.exposeInMainWorld(
- "mainApi", {
- invoke: async (channel, ...args) => {
- if (validToMain.includes(channel)) {
- try {
- const res = await ipcRenderer.invoke(channel, ...args);
- return res;
- } catch (e) {
- console.log("IPCRenderer.invoke error");
- }
- }
- },
- send: (channel, ...args) => {
- if (validToMain.includes(channel)) {
- ipcRenderer.send(channel, ...args);
- }
- },
- on: (channel, func) => {
- if (validFromMainChannels.includes(channel)) {
- ipcRenderer.on(channel, (event, ...args) => func(...args));
- }
- },
- removeAllListeners: (channel) => {
- ipcRenderer.removeAllListeners(channel);
- }
- }
-);
diff --git a/src/render/preload.ts b/src/render/preload.ts
new file mode 100644
index 0000000..054f672
--- /dev/null
+++ b/src/render/preload.ts
@@ -0,0 +1,38 @@
+// All of the Node.js APIs are available in the preload process.
+// It has the same sandbox as a Chrome extension.
+import { ipcRenderer } from "electron";
+
+declare global {
+ interface Window {
+ ipcRenderer: typeof ipcRenderer;
+ }
+}
+
+window.ipcRenderer = ipcRenderer;
+
+process.once("loaded", () => {
+
+ window.addEventListener("message", event => {
+
+ // do something with custom event
+ const message = event.data;
+
+ if (message.endpoint === "update-menu-bar") {
+ ipcRenderer.send("update-menu-bar", message.content);
+ }
+
+ if (message.endpoint === "update-recorder") {
+ ipcRenderer.send("update-recorder", message.content);
+ }
+
+ if (message.endpoint === "client-message") {
+ ipcRenderer.send("client-message", message.content);
+ }
+
+ if (message.endpoint === "logout") {
+ ipcRenderer.send("logout", message.content);
+ }
+
+ });
+
+});
diff --git a/src/render/shims-vue.d.ts b/src/render/shims-vue.d.ts
new file mode 100644
index 0000000..35a61a9
--- /dev/null
+++ b/src/render/shims-vue.d.ts
@@ -0,0 +1,13 @@
+declare module "*.vue" {
+ import { defineComponent } from "vue";
+ const component: ReturnType;
+ export default component;
+}
+
+declare module "anime-js" {
+ namespace anime {
+ import * as anime from "animejs/lib/anime.es.js";
+ function anime(): void;
+ export = anime;
+ }
+}
diff --git a/src/render/vendor/vue.js b/src/render/vendor/vue.js
deleted file mode 100644
index 6e5ee00..0000000
--- a/src/render/vendor/vue.js
+++ /dev/null
@@ -1,15872 +0,0 @@
-var Vue = (function (exports) {
- 'use strict';
-
- /**
- * Make a map and return a function for checking if a key
- * is in that map.
- * IMPORTANT: all calls of this function must be prefixed with
- * \/\*#\_\_PURE\_\_\*\/
- * So that rollup can tree-shake them if necessary.
- */
- function makeMap(str, expectsLowerCase) {
- const map = Object.create(null);
- const list = str.split(',');
- for (let i = 0; i < list.length; i++) {
- map[list[i]] = true;
- }
- return expectsLowerCase ? val => !!map[val.toLowerCase()] : val => !!map[val];
- }
-
- /**
- * dev only flag -> name mapping
- */
- const PatchFlagNames = {
- [1 /* TEXT */]: `TEXT`,
- [2 /* CLASS */]: `CLASS`,
- [4 /* STYLE */]: `STYLE`,
- [8 /* PROPS */]: `PROPS`,
- [16 /* FULL_PROPS */]: `FULL_PROPS`,
- [32 /* HYDRATE_EVENTS */]: `HYDRATE_EVENTS`,
- [64 /* STABLE_FRAGMENT */]: `STABLE_FRAGMENT`,
- [128 /* KEYED_FRAGMENT */]: `KEYED_FRAGMENT`,
- [256 /* UNKEYED_FRAGMENT */]: `UNKEYED_FRAGMENT`,
- [512 /* NEED_PATCH */]: `NEED_PATCH`,
- [1024 /* DYNAMIC_SLOTS */]: `DYNAMIC_SLOTS`,
- [2048 /* DEV_ROOT_FRAGMENT */]: `DEV_ROOT_FRAGMENT`,
- [-1 /* HOISTED */]: `HOISTED`,
- [-2 /* BAIL */]: `BAIL`
- };
-
- /**
- * Dev only
- */
- const slotFlagsText = {
- [1 /* STABLE */]: 'STABLE',
- [2 /* DYNAMIC */]: 'DYNAMIC',
- [3 /* FORWARDED */]: 'FORWARDED'
- };
-
- const GLOBALS_WHITE_LISTED = 'Infinity,undefined,NaN,isFinite,isNaN,parseFloat,parseInt,decodeURI,' +
- 'decodeURIComponent,encodeURI,encodeURIComponent,Math,Number,Date,Array,' +
- 'Object,Boolean,String,RegExp,Map,Set,JSON,Intl,BigInt';
- const isGloballyWhitelisted = /*#__PURE__*/ makeMap(GLOBALS_WHITE_LISTED);
-
- const range = 2;
- function generateCodeFrame(source, start = 0, end = source.length) {
- // Split the content into individual lines but capture the newline sequence
- // that separated each line. This is important because the actual sequence is
- // needed to properly take into account the full line length for offset
- // comparison
- let lines = source.split(/(\r?\n)/);
- // Separate the lines and newline sequences into separate arrays for easier referencing
- const newlineSequences = lines.filter((_, idx) => idx % 2 === 1);
- lines = lines.filter((_, idx) => idx % 2 === 0);
- let count = 0;
- const res = [];
- for (let i = 0; i < lines.length; i++) {
- count +=
- lines[i].length +
- ((newlineSequences[i] && newlineSequences[i].length) || 0);
- if (count >= start) {
- for (let j = i - range; j <= i + range || end > count; j++) {
- if (j < 0 || j >= lines.length)
- continue;
- const line = j + 1;
- res.push(`${line}${' '.repeat(Math.max(3 - String(line).length, 0))}| ${lines[j]}`);
- const lineLength = lines[j].length;
- const newLineSeqLength = (newlineSequences[j] && newlineSequences[j].length) || 0;
- if (j === i) {
- // push underline
- const pad = start - (count - (lineLength + newLineSeqLength));
- const length = Math.max(1, end > count ? lineLength - pad : end - start);
- res.push(` | ` + ' '.repeat(pad) + '^'.repeat(length));
- }
- else if (j > i) {
- if (end > count) {
- const length = Math.max(Math.min(end - count, lineLength), 1);
- res.push(` | ` + '^'.repeat(length));
- }
- count += lineLength + newLineSeqLength;
- }
- }
- break;
- }
- }
- return res.join('\n');
- }
-
- /**
- * On the client we only need to offer special cases for boolean attributes that
- * have different names from their corresponding dom properties:
- * - itemscope -> N/A
- * - allowfullscreen -> allowFullscreen
- * - formnovalidate -> formNoValidate
- * - ismap -> isMap
- * - nomodule -> noModule
- * - novalidate -> noValidate
- * - readonly -> readOnly
- */
- const specialBooleanAttrs = `itemscope,allowfullscreen,formnovalidate,ismap,nomodule,novalidate,readonly`;
- const isSpecialBooleanAttr = /*#__PURE__*/ makeMap(specialBooleanAttrs);
- /**
- * Boolean attributes should be included if the value is truthy or ''.
- * e.g.