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 @@ + + + + + + + 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 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. - const forcePatchValue = (type === 'input' && dirs) || type === 'option'; - // skip props & children if this is hoisted static nodes - if (forcePatchValue || patchFlag !== -1 /* HOISTED */) { - if (dirs) { - invokeDirectiveHook(vnode, null, parentComponent, 'created'); - } - // props - if (props) { - if (forcePatchValue || - !optimized || - patchFlag & (16 /* FULL_PROPS */ | 32 /* HYDRATE_EVENTS */)) { - for (const key in props) { - if ((forcePatchValue && key.endsWith('value')) || - (isOn(key) && !isReservedProp(key))) { - patchProp(el, key, null, props[key]); - } - } - } - else if (props.onClick) { - // Fast path for click listeners (which is most often) to avoid - // iterating through props. - patchProp(el, 'onClick', null, props.onClick); - } - } - // vnode / directive hooks - let vnodeHooks; - if ((vnodeHooks = props && props.onVnodeBeforeMount)) { - invokeVNodeHook(vnodeHooks, parentComponent, vnode); - } - if (dirs) { - invokeDirectiveHook(vnode, null, parentComponent, 'beforeMount'); - } - if ((vnodeHooks = props && props.onVnodeMounted) || dirs) { - queueEffectWithSuspense(() => { - vnodeHooks && invokeVNodeHook(vnodeHooks, parentComponent, vnode); - dirs && invokeDirectiveHook(vnode, null, parentComponent, 'mounted'); - }, parentSuspense); - } - // children - if (shapeFlag & 16 /* ARRAY_CHILDREN */ && - // skip if element has innerHTML / textContent - !(props && (props.innerHTML || props.textContent))) { - let next = hydrateChildren(el.firstChild, vnode, el, parentComponent, parentSuspense, slotScopeIds, optimized); - let hasWarned = false; - while (next) { - hasMismatch = true; - if (!hasWarned) { - warn$1(`Hydration children mismatch in <${vnode.type}>: ` + - `server rendered element contains more child nodes than client vdom.`); - hasWarned = true; - } - // The SSRed DOM contains more nodes than it should. Remove them. - const cur = next; - next = next.nextSibling; - remove(cur); - } - } - else if (shapeFlag & 8 /* TEXT_CHILDREN */) { - if (el.textContent !== vnode.children) { - hasMismatch = true; - warn$1(`Hydration text content mismatch in <${vnode.type}>:\n` + - `- Client: ${el.textContent}\n` + - `- Server: ${vnode.children}`); - el.textContent = vnode.children; - } - } - } - return el.nextSibling; - }; - const hydrateChildren = (node, parentVNode, container, parentComponent, parentSuspense, slotScopeIds, optimized) => { - optimized = optimized || !!parentVNode.dynamicChildren; - const children = parentVNode.children; - const l = children.length; - let hasWarned = false; - for (let i = 0; i < l; i++) { - const vnode = optimized - ? children[i] - : (children[i] = normalizeVNode(children[i])); - if (node) { - node = hydrateNode(node, vnode, parentComponent, parentSuspense, slotScopeIds, optimized); - } - else if (vnode.type === Text && !vnode.children) { - continue; - } - else { - hasMismatch = true; - if (!hasWarned) { - warn$1(`Hydration children mismatch in <${container.tagName.toLowerCase()}>: ` + - `server rendered element contains fewer child nodes than client vdom.`); - hasWarned = true; - } - // the SSRed DOM didn't contain enough nodes. Mount the missing ones. - patch(null, vnode, container, null, parentComponent, parentSuspense, isSVGContainer(container), slotScopeIds); - } - } - return node; - }; - const hydrateFragment = (node, vnode, parentComponent, parentSuspense, slotScopeIds, optimized) => { - const { slotScopeIds: fragmentSlotScopeIds } = vnode; - if (fragmentSlotScopeIds) { - slotScopeIds = slotScopeIds - ? slotScopeIds.concat(fragmentSlotScopeIds) - : fragmentSlotScopeIds; - } - const container = parentNode(node); - const next = hydrateChildren(nextSibling(node), vnode, container, parentComponent, parentSuspense, slotScopeIds, optimized); - if (next && isComment(next) && next.data === ']') { - return nextSibling((vnode.anchor = next)); - } - else { - // fragment didn't hydrate successfully, since we didn't get a end anchor - // back. This should have led to node/children mismatch warnings. - hasMismatch = true; - // since the anchor is missing, we need to create one and insert it - insert((vnode.anchor = createComment(`]`)), container, next); - return next; - } - }; - const handleMismatch = (node, vnode, parentComponent, parentSuspense, slotScopeIds, isFragment) => { - hasMismatch = true; - warn$1(`Hydration node mismatch:\n- Client vnode:`, vnode.type, `\n- Server rendered DOM:`, node, node.nodeType === 3 /* TEXT */ - ? `(text)` - : isComment(node) && node.data === '[' - ? `(start of fragment)` - : ``); - vnode.el = null; - if (isFragment) { - // remove excessive fragment nodes - const end = locateClosingAsyncAnchor(node); - while (true) { - const next = nextSibling(node); - if (next && next !== end) { - remove(next); - } - else { - break; - } - } - } - const next = nextSibling(node); - const container = parentNode(node); - remove(node); - patch(null, vnode, container, next, parentComponent, parentSuspense, isSVGContainer(container), slotScopeIds); - return next; - }; - const locateClosingAsyncAnchor = (node) => { - let match = 0; - while (node) { - node = nextSibling(node); - if (node && isComment(node)) { - if (node.data === '[') - match++; - if (node.data === ']') { - if (match === 0) { - return nextSibling(node); - } - else { - match--; - } - } - } - } - return node; - }; - return [hydrate, hydrateNode]; - } - - let supported; - let perf; - function startMeasure(instance, type) { - if (instance.appContext.config.performance && isSupported()) { - perf.mark(`vue-${type}-${instance.uid}`); - } - { - devtoolsPerfStart(instance, type, supported ? perf.now() : Date.now()); - } - } - function endMeasure(instance, type) { - if (instance.appContext.config.performance && isSupported()) { - const startTag = `vue-${type}-${instance.uid}`; - const endTag = startTag + `:end`; - perf.mark(endTag); - perf.measure(`<${formatComponentName(instance, instance.type)}> ${type}`, startTag, endTag); - perf.clearMarks(startTag); - perf.clearMarks(endTag); - } - { - devtoolsPerfEnd(instance, type, supported ? perf.now() : Date.now()); - } - } - function isSupported() { - if (supported !== undefined) { - return supported; - } - /* eslint-disable no-restricted-globals */ - if (typeof window !== 'undefined' && window.performance) { - supported = true; - perf = window.performance; - } - else { - supported = false; - } - /* eslint-enable no-restricted-globals */ - return supported; - } - - const queuePostRenderEffect = queueEffectWithSuspense - ; - /** - * The createRenderer function accepts two generic arguments: - * HostNode and HostElement, corresponding to Node and Element types in the - * host environment. For example, for runtime-dom, HostNode would be the DOM - * `Node` interface and HostElement would be the DOM `Element` interface. - * - * Custom renderers can pass in the platform specific types like this: - * - * ``` js - * const { render, createApp } = createRenderer({ - * patchProp, - * ...nodeOps - * }) - * ``` - */ - function createRenderer(options) { - return baseCreateRenderer(options); - } - // Separate API for creating hydration-enabled renderer. - // Hydration logic is only used when calling this function, making it - // tree-shakable. - function createHydrationRenderer(options) { - return baseCreateRenderer(options, createHydrationFunctions); - } - // implementation - function baseCreateRenderer(options, createHydrationFns) { - { - const target = getGlobalThis(); - target.__VUE__ = true; - setDevtoolsHook(target.__VUE_DEVTOOLS_GLOBAL_HOOK__); - } - const { insert: hostInsert, remove: hostRemove, patchProp: hostPatchProp, createElement: hostCreateElement, createText: hostCreateText, createComment: hostCreateComment, setText: hostSetText, setElementText: hostSetElementText, parentNode: hostParentNode, nextSibling: hostNextSibling, setScopeId: hostSetScopeId = NOOP, cloneNode: hostCloneNode, insertStaticContent: hostInsertStaticContent } = options; - // Note: functions inside this closure should use `const xxx = () => {}` - // style in order to prevent being inlined by minifiers. - const patch = (n1, n2, container, anchor = null, parentComponent = null, parentSuspense = null, isSVG = false, slotScopeIds = null, optimized = isHmrUpdating ? false : !!n2.dynamicChildren) => { - if (n1 === n2) { - return; - } - // patching & not same type, unmount old tree - if (n1 && !isSameVNodeType(n1, n2)) { - anchor = getNextHostNode(n1); - unmount(n1, parentComponent, parentSuspense, true); - n1 = null; - } - if (n2.patchFlag === -2 /* BAIL */) { - optimized = false; - n2.dynamicChildren = null; - } - const { type, ref, shapeFlag } = n2; - switch (type) { - case Text: - processText(n1, n2, container, anchor); - break; - case Comment: - processCommentNode(n1, n2, container, anchor); - break; - case Static: - if (n1 == null) { - mountStaticNode(n2, container, anchor, isSVG); - } - else { - patchStaticNode(n1, n2, container, isSVG); - } - break; - case Fragment: - processFragment(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized); - break; - default: - if (shapeFlag & 1 /* ELEMENT */) { - processElement(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized); - } - else if (shapeFlag & 6 /* COMPONENT */) { - processComponent(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized); - } - else if (shapeFlag & 64 /* TELEPORT */) { - type.process(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized, internals); - } - else if (shapeFlag & 128 /* SUSPENSE */) { - type.process(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized, internals); - } - else { - warn$1('Invalid VNode type:', type, `(${typeof type})`); - } - } - // set ref - if (ref != null && parentComponent) { - setRef(ref, n1 && n1.ref, parentSuspense, n2 || n1, !n2); - } - }; - const processText = (n1, n2, container, anchor) => { - if (n1 == null) { - hostInsert((n2.el = hostCreateText(n2.children)), container, anchor); - } - else { - const el = (n2.el = n1.el); - if (n2.children !== n1.children) { - hostSetText(el, n2.children); - } - } - }; - const processCommentNode = (n1, n2, container, anchor) => { - if (n1 == null) { - hostInsert((n2.el = hostCreateComment(n2.children || '')), container, anchor); - } - else { - // there's no support for dynamic comments - n2.el = n1.el; - } - }; - const mountStaticNode = (n2, container, anchor, isSVG) => { - [n2.el, n2.anchor] = hostInsertStaticContent(n2.children, container, anchor, isSVG); - }; - /** - * Dev / HMR only - */ - const patchStaticNode = (n1, n2, container, isSVG) => { - // static nodes are only patched during dev for HMR - if (n2.children !== n1.children) { - const anchor = hostNextSibling(n1.anchor); - // remove existing - removeStaticNode(n1); - [n2.el, n2.anchor] = hostInsertStaticContent(n2.children, container, anchor, isSVG); - } - else { - n2.el = n1.el; - n2.anchor = n1.anchor; - } - }; - const moveStaticNode = ({ el, anchor }, container, nextSibling) => { - let next; - while (el && el !== anchor) { - next = hostNextSibling(el); - hostInsert(el, container, nextSibling); - el = next; - } - hostInsert(anchor, container, nextSibling); - }; - const removeStaticNode = ({ el, anchor }) => { - let next; - while (el && el !== anchor) { - next = hostNextSibling(el); - hostRemove(el); - el = next; - } - hostRemove(anchor); - }; - const processElement = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized) => { - isSVG = isSVG || n2.type === 'svg'; - if (n1 == null) { - mountElement(n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized); - } - else { - patchElement(n1, n2, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized); - } - }; - const mountElement = (vnode, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized) => { - let el; - let vnodeHook; - const { type, props, shapeFlag, transition, patchFlag, dirs } = vnode; - { - el = vnode.el = hostCreateElement(vnode.type, isSVG, props && props.is, props); - // mount children first, since some props may rely on child content - // being already rendered, e.g. `