diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index eaf98f7..0000000 --- a/.eslintrc.js +++ /dev/null @@ -1,38 +0,0 @@ -module.exports = { - root: true, - env: { - node: true - }, - extends: [ - "plugin:vue/vue3-essential", - "eslint:recommended", - "@vue/typescript/recommended" - ], - parserOptions: { - ecmaVersion: 2020 - }, - rules: { - "no-console": process.env.NODE_ENV === "production" ? "warn" : "off", - "no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off", - "no-unused-vars": "off", - "@typescript-eslint/no-unused-vars": [ - "error", - { "argsIgnorePattern": "^_" } - ] - }, - overrides: [ - { - files: [ - "**/__tests__/*.{j,t}s?(x)", - "**/tests/unit/**/*.spec.{j,t}s?(x)", - "*.js" - ], - rules: { - "@typescript-eslint/no-var-requires": "off", - }, - env: { - jest: true - } - } - ] -}; diff --git a/.gitignore b/.gitignore index 20128fc..f8cd566 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,5 @@ .DS_Store -node_modules -/dist -/crate/target/* +/node_modules /.log crash.log @@ -13,8 +11,6 @@ crash.log # Log files npm-debug.log* -yarn-debug.log* -yarn-error.log* pnpm-debug.log* # Editor directories and files @@ -26,12 +22,8 @@ pnpm-debug.log* *.sln *.sw? -#Electron-builder output -/dist_electron +# Dist +*.app -#Electron-builder config -electron-builder.yml - -#Window and session states -session.json -window.json +*.sublime-workspace +*.sublime-project diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml deleted file mode 100644 index 2c0d22c..0000000 --- a/.gitlab-ci.yml +++ /dev/null @@ -1,63 +0,0 @@ -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 new file mode 100644 index 0000000..ce46d51 --- /dev/null +++ b/.npmrc @@ -0,0 +1,2 @@ +@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 c13a8ab..c583975 100644 --- a/README.md +++ b/README.md @@ -27,3 +27,36 @@ 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 + +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 deleted file mode 100644 index e955840..0000000 --- a/babel.config.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = { - presets: [ - '@vue/cli-plugin-babel/preset' - ] -} diff --git a/entitlements.plist b/entitlements.plist new file mode 100644 index 0000000..0555172 --- /dev/null +++ b/entitlements.plist @@ -0,0 +1,14 @@ + + + + + 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 new file mode 100644 index 0000000..2897b98 Binary files /dev/null and b/icon.png differ diff --git a/jest.config.js b/jest.config.js deleted file mode 100644 index d7f13cc..0000000 --- a/jest.config.js +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = { - preset: '@vue/cli-plugin-unit-jest/presets/typescript-and-babel', - transform: { - '^.+\\.vue$': 'vue-jest' - } -} diff --git a/jsconfig.json b/jsconfig.json deleted file mode 100644 index 5df65d0..0000000 --- a/jsconfig.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "typeAcquisition": { - - "enable": true - } -} \ No newline at end of file diff --git a/notarize.js b/notarize.js new file mode 100644 index 0000000..cd3b19a --- /dev/null +++ b/notarize.js @@ -0,0 +1,52 @@ +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 new file mode 100644 index 0000000..cc9403e --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1059 @@ +{ + "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 24d13b1..6b6c329 100644 --- a/package.json +++ b/package.json @@ -1,104 +1,25 @@ { "name": "Crimata", - "version": "0.9.9", + "productName": "crimata-messenger", + "version": "1.0.0", "private": true, - "description": "Cross-platform messenger application built with electron, vue3, and TS.", + "description": "Cross-platform messenger app (Electron, Vue3)", "author": { - "name": "Enrique Hernandez" + "name": "Andrew Gundersen" }, + "main": "src/main.js", "scripts": { - "build": "vue-cli-service electron:build", - "dev": "vue-cli-service electron:serve", - "postinstall": "electron-builder install-app-deps", - "postuninstall": "electron-builder install-app-deps" + "start": "electron ." }, - "main": "init.js", "dependencies": { - "@google-cloud/speech": "^4.2.0", - "@types/animejs": "^3.1.2", - "@types/bindings": "^1.3.0", - "@types/dom-mediacapture-record": "^1.0.7", - "@types/node": "^14.14.25", - "@types/uuid": "^8.3.0", - "@types/ws": "^7.2.7", - "animejs": "^3.2.0", + "@crimata/nodeaudio": "0.0.0", "axios": "^0.21.1", - "core-js": "^3.6.5", - "electron-is-dev": "^2.0.0", + "base64-arraybuffer": "^1.0.1", "electron-store": "^8.0.0", - "electron-updater": "^4.3.8", - "mitt": "^2.1.0", - "naudiodon": "^2.3.2", - "node-record-lpcm16": "^1.0.1", - "update-electron-app": "^2.0.1", - "uuid": "^8.3.2", - "vue": "^3.0.0-0", - "vue-router": "^4.0.0-0", - "vuex": "^4.0.0-0", "ws": "^7.3.1" }, "devDependencies": { - "@types/axios": "^0.14.0", - "@types/electron-devtools-installer": "^2.2.0", - "@types/jest": "^24.0.19", - "@typescript-eslint/eslint-plugin": "^2.33.0", - "@typescript-eslint/parser": "^2.33.0", - "@vue/cli-plugin-babel": "~4.5.0", - "@vue/cli-plugin-eslint": "~4.5.0", - "@vue/cli-plugin-router": "~4.5.0", - "@vue/cli-plugin-typescript": "~4.5.0", - "@vue/cli-plugin-unit-jest": "~4.5.0", - "@vue/cli-plugin-vuex": "~4.5.0", - "@vue/cli-service": "~4.5.0", - "@vue/compiler-sfc": "^3.0.0-0", - "@vue/eslint-config-typescript": "^5.0.2", - "@vue/test-utils": "^2.0.0-0", - "@wasm-tool/wasm-pack-plugin": "^1.3.1", - "electron": "^9.0.0", - "electron-devtools-installer": "^3.1.0", - "electron-log": "^4.3.4", - "eslint": "^6.7.2", - "eslint-plugin-vue": "^7.0.0-0", - "lint-staged": "^9.5.0", - "node-sass": "^4.12.0", - "optimize-wasm-webpack-plugin": "^1.0.12", - "sass-loader": "^8.0.2", - "spectron": "11.0.0", - "typescript": "~3.9.3", - "vue-cli-plugin-electron-builder": "~2.0.0-rc.6", - "vue-jest": "^5.0.0-0" - }, - "vue": { - "lintOnSave": false, - "pluginOptions": { - "electronBuilder": { - "mainProcessFile": "./src/init.ts", - "rendererProcessFile": "./src/render/main.ts", - "preload": "./src/render/preload.ts", - "builderOptions": { - "appId": "com.crimata.ElectronUpdaterApp", - "artifactName": "${productName}-${version}.${ext}", - "publish": { - "provider": "generic", - "url": "https://gitlab.com/api/v4/projects/25637892/jobs/artifacts/main/raw/dist_electron?job=build" - } - } - } - } - }, - "gitHooks": { - "pre-commit": "lint-staged" - }, - "lint-staged": { - "*.{js,jsx,vue,ts,tsx}": [ - "vue-cli-service lint", - "git add" - ] - }, - "productName": "crimata-messenger", - "repository": { - "type": "git", - "url": "https://gitlab.com/crimata/electron-app.git", - "release": "latest" + "electron-notarize": "^1.1.1", + "electron-osx-sign": "^0.6.0" } } diff --git a/public/index.html b/public/index.html deleted file mode 100644 index 8f79d27..0000000 --- a/public/index.html +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - <%= htmlWebpackPlugin.options.title %> - - - -
- - - diff --git a/src/account.js b/src/account.js new file mode 100644 index 0000000..e339db2 --- /dev/null +++ b/src/account.js @@ -0,0 +1,41 @@ +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 deleted file mode 100644 index 581da0e..0000000 --- a/src/account.ts +++ /dev/null @@ -1,95 +0,0 @@ - -import { postAuth, postLogin, postLogout } from "@/api/account"; -import { endSession, launchSession } from "@/session"; -import { getToken, setToken, setProfile, getProfile, clearStore } from "./store"; -import { parseAuthRes } from "./auth"; -import { ipcEmit } from "@/composables/useEmitter"; - -export const accountAuth = async (): Promise => { - - /* 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 new file mode 100644 index 0000000..785c304 --- /dev/null +++ b/src/api.js @@ -0,0 +1,38 @@ +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 deleted file mode 100644 index 45baca2..0000000 --- a/src/api/account.ts +++ /dev/null @@ -1,31 +0,0 @@ - -import useHttp from "@/composables/useHttp"; -import axios from "axios"; -import {config} from "@/config"; - -const { post } = useHttp(); - -export const postAuth = async (token: string) => ( - await axios({ - url: config.BUSINESS_URL + config.BUSINESS_PREFIX + '/account/authenticate', - headers: { - Cookie: `crimataCookie=${token}` - }, - method: 'POST', - }) -); - - -export const postLogin = async (email: string, password: string) => ( - await post('/account/login', { email, password }) -); - - -export const postLogout = - async (): Promise => (await post('/account/logout')); - - - - - - diff --git a/src/assets/icon.icns b/src/assets/icon.icns new file mode 100644 index 0000000..b173533 Binary files /dev/null and b/src/assets/icon.icns differ diff --git a/src/assets/tray/000.png b/src/assets/tray/000.png new file mode 100644 index 0000000..e6d5d32 Binary files /dev/null and b/src/assets/tray/000.png differ diff --git a/src/assets/tray/000@2x.png b/src/assets/tray/000@2x.png new file mode 100644 index 0000000..6d61d7f Binary files /dev/null and b/src/assets/tray/000@2x.png differ diff --git a/src/assets/tray/001.png b/src/assets/tray/001.png new file mode 100644 index 0000000..8d65b38 Binary files /dev/null and b/src/assets/tray/001.png differ diff --git a/src/assets/tray/001@2x.png b/src/assets/tray/001@2x.png new file mode 100644 index 0000000..b7f0e2c Binary files /dev/null and b/src/assets/tray/001@2x.png differ diff --git a/src/assets/tray/010.png b/src/assets/tray/010.png new file mode 100644 index 0000000..bc5718c Binary files /dev/null and b/src/assets/tray/010.png differ diff --git a/src/assets/tray/010@2x.png b/src/assets/tray/010@2x.png new file mode 100644 index 0000000..6b97193 Binary files /dev/null and b/src/assets/tray/010@2x.png differ diff --git a/src/assets/tray/011.png b/src/assets/tray/011.png new file mode 100644 index 0000000..c2f8612 Binary files /dev/null and b/src/assets/tray/011.png differ diff --git a/src/assets/tray/011@2x.png b/src/assets/tray/011@2x.png new file mode 100644 index 0000000..befcd5f Binary files /dev/null and b/src/assets/tray/011@2x.png differ diff --git a/src/assets/tray/100.png b/src/assets/tray/100.png new file mode 100644 index 0000000..99df7df Binary files /dev/null and b/src/assets/tray/100.png differ diff --git a/src/assets/tray/100@2x.png b/src/assets/tray/100@2x.png new file mode 100644 index 0000000..2d26144 Binary files /dev/null and b/src/assets/tray/100@2x.png differ diff --git a/src/assets/tray/101.png b/src/assets/tray/101.png new file mode 100644 index 0000000..523bc93 Binary files /dev/null and b/src/assets/tray/101.png differ diff --git a/src/assets/tray/101@2x.png b/src/assets/tray/101@2x.png new file mode 100644 index 0000000..91a15be Binary files /dev/null and b/src/assets/tray/101@2x.png differ diff --git a/src/assets/tray/110.png b/src/assets/tray/110.png new file mode 100644 index 0000000..ddd2524 Binary files /dev/null and b/src/assets/tray/110.png differ diff --git a/src/assets/tray/110@2x.png b/src/assets/tray/110@2x.png new file mode 100644 index 0000000..fd073dd Binary files /dev/null and b/src/assets/tray/110@2x.png differ diff --git a/src/assets/tray/111.png b/src/assets/tray/111.png new file mode 100644 index 0000000..d2dd686 Binary files /dev/null and b/src/assets/tray/111.png differ diff --git a/src/assets/tray/111@2x.png b/src/assets/tray/111@2x.png new file mode 100644 index 0000000..2bc77fd Binary files /dev/null and b/src/assets/tray/111@2x.png differ diff --git a/src/audio.js b/src/audio.js new file mode 100644 index 0000000..73223b2 --- /dev/null +++ b/src/audio.js @@ -0,0 +1,134 @@ +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 deleted file mode 100644 index 7ae2562..0000000 --- a/src/audio.ts +++ /dev/null @@ -1,187 +0,0 @@ -/* eslint @typescript-eslint/no-var-requires: "off" */ - -"use strict"; - -// where the audio goes -let buffer: ArrayBuffer[] = []; - -// place audio data in buffer -export const collect: IpcListenerCallback = (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 deleted file mode 100644 index 14ed36a..0000000 --- a/src/auth.ts +++ /dev/null @@ -1,13 +0,0 @@ - -export const parseAuthRes = (authRes: any) => { - const token = authRes.headers['set-cookie'][0].split(";")[0].split("=")[1] as string; - const profile = authRes.data as Profile; - return { - token, - profile - } -}; - - - - diff --git a/src/codec.js b/src/codec.js new file mode 100644 index 0000000..3a9a8bf --- /dev/null +++ b/src/codec.js @@ -0,0 +1,13 @@ +const { ipcMain } = require("electron"); +const { encode, decode } = require("base64-arraybuffer"); + +ipcMain.handle("encode", (_e, arrayBuffer) => { + return encode(arrayBuffer); +}); + +ipcMain.handle("decode", (_e, b64string) => { + return decode(b64string); +}); + +exports.encode = encode; +exports.decode = decode; \ No newline at end of file diff --git a/src/composables/autoUpdate.ts b/src/composables/autoUpdate.ts deleted file mode 100644 index 1a6b780..0000000 --- a/src/composables/autoUpdate.ts +++ /dev/null @@ -1,31 +0,0 @@ -const { autoUpdater } = require('electron-updater'); - -let win: boolean; - -// Listen for window creation. -backgroundMitt.on('window-active', (state: boolean) => { - win = state; -}); - -// Auto updating. -autoUpdater.requestHeaders = { 'PRIVATE-TOKEN': 'mvvgWYwWnot4bisiQMh_' } - -autoUpdater.on('update-available', (info: any) => { - console.log(`Update available: ${info.version}`) -}) - -autoUpdater.on('update-downloaded', (info: any) => { - - const updateDialog = { - type: 'info', - buttons: ['Restart', 'Later'], - title: 'Application Update', - message: info.version, - detail: 'A new version has been downloaded. Restart the application to apply the updates.' - } - - dialog.showMessageBox(updateDialog).then((returnValue) => { - if (returnValue.response === 0) autoUpdater.quitAndInstall() - }) - -}) \ No newline at end of file diff --git a/src/composables/useEmitter.ts b/src/composables/useEmitter.ts deleted file mode 100644 index f95f328..0000000 --- a/src/composables/useEmitter.ts +++ /dev/null @@ -1,16 +0,0 @@ - -// 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 deleted file mode 100644 index 6b9ee56..0000000 --- a/src/composables/useHttp.ts +++ /dev/null @@ -1,51 +0,0 @@ - -import axios, { AxiosRequestConfig } from 'axios'; -import {config} from "@/config"; - -const baseURL = config.BUSINESS_URL + config.BUSINESS_PREFIX; - -interface Request { - endpoint: string; - query?: Record; - 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 deleted file mode 100644 index ebfc4bc..0000000 --- a/src/composables/useIpcMain.ts +++ /dev/null @@ -1,82 +0,0 @@ - -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 deleted file mode 100644 index c85f7ed..0000000 --- a/src/composables/useMessageCanvas.ts +++ /dev/null @@ -1,36 +0,0 @@ - - - -export default class Canvas { - - messages: Message[]; - - /* seed canvas with messages on init */ - constructor(messages: Message[]) { - this.messages = messages; - ipcEmit("seed-view", this.messages); - } - - /* add a new message to the canvas */ - add(message: Message) { - this.messages.push(message); - ipcEmit("update-view", message); - } - - /* update an existing message */ - update(message: Message) { - - /* get the target message */ - let target_message = this.messages.filter((m: Message) => { - return m.uid = message.uid; - })[0]; - - /* replace the target message */ - if (target_message) { - target_message = message; - ipcEmit("update-view", message); - } - - } - -} diff --git a/src/composables/useSaveToJSON.ts b/src/composables/useSaveToJSON.ts deleted file mode 100644 index 06ab4b9..0000000 --- a/src/composables/useSaveToJSON.ts +++ /dev/null @@ -1,13 +0,0 @@ - -import {config} from "@/config"; -import fs from 'fs'; - -export const saveToJson = (fileName: string, data: any) => { - - fs.writeFile(config.configPath + fileName, JSON.stringify(data), (err) => { - if (err) { - console.log("Error when saving to json.") - } - }) - -} diff --git a/src/composables/useWebsockets.ts b/src/composables/useWebsockets.ts deleted file mode 100644 index d1cfc02..0000000 --- a/src/composables/useWebsockets.ts +++ /dev/null @@ -1,89 +0,0 @@ - -"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 new file mode 100644 index 0000000..bc47fb5 --- /dev/null +++ b/src/config.js @@ -0,0 +1,11 @@ +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 deleted file mode 100644 index 855b1b2..0000000 --- a/src/config.ts +++ /dev/null @@ -1,17 +0,0 @@ - -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 deleted file mode 100644 index 9a0fdf6..0000000 --- a/src/init.ts +++ /dev/null @@ -1,52 +0,0 @@ -/** - * Entry point for Crimata electron app. - * "Look on my Works, ye Mighty, and despair!" - */ - -"use strict"; - -import { app, protocol } from "electron"; -import createWindow from "./window"; -import main from "./main"; -import { backgroundMitt } from '@/composables/useEmitter'; - -console.log('Starting Crimata electron app.'); - -// Scheme must be registered before the app is ready -protocol.registerSchemesAsPrivileged([ - { scheme: "app", privileges: { secure: true, standard: true } } -]); - -const isDev = require('electron-is-dev'); - -let win: boolean; - -// Listen for window creation. -backgroundMitt.on('window-active', (state: boolean) => { - win = state; -}); - -/* Start main process on ready */ -app.on("ready", async () => { - await main(); -}); - -// Must keep to ensure app doesn't quit on close. -app.on("before-quit", async () => { -}); - -// Must keep to ensure app doesn't quit on close. -app.on("window-all-closed", () => { -}); - -// When user clicks app icon (re-open) -app.on("activate", () => { - if (!win) createWindow(); -}); - -// Exit cleanly on request from parent process in development mode. -if (isDev) { - process.on("SIGTERM", () => { - app.quit(); - }); -} diff --git a/src/io.js b/src/io.js new file mode 100644 index 0000000..f09d3cf --- /dev/null +++ b/src/io.js @@ -0,0 +1,69 @@ +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 deleted file mode 100644 index 5529662..0000000 --- a/src/ipc/handlers.ts +++ /dev/null @@ -1,28 +0,0 @@ - -"use strict"; - -import { accountLogin, accountLogout, accountProfile } from "@/account"; -import { IpcHandler } from "@/composables/useIpcMain"; -import { flush } from "@/audio"; - - -const LOGIN_CHANNEL = "invoke-account-login"; -const LOGOUT_CHANNEL = "invoke-account-logout"; -const GET_AUDIO_CHANNEL = "invoke-audio-flush"; - -export const loginHandler = new IpcHandler({ - channel: LOGIN_CHANNEL, - handlerCallback: accountLogin -}); - -export const logoutHandler = new IpcHandler({ - channel: LOGOUT_CHANNEL, - handlerCallback: accountLogout -}); - -export const getAudioHandler = new IpcHandler({ - channel: GET_AUDIO_CHANNEL, - handlerCallback: flush -}); - - diff --git a/src/ipc/index.ts b/src/ipc/index.ts deleted file mode 100644 index e6eb90a..0000000 --- a/src/ipc/index.ts +++ /dev/null @@ -1,33 +0,0 @@ - -"use strict"; - -import * as handlers from "./handlers"; -import * as listeners from "./listeners"; - -const ipcHandlers: IPCHandlers = {}; -const ipcListeners: IPCListeners = {}; - -const _initHandlers = (): void => { - for (const [key, handler] of Object.entries(handlers)) { - if (!(key in ipcHandlers)) { - ipcHandlers[key] = handler; - handler.handle(); - } - } -}; - -const _initListeners = (): void => { - for (const [key, listener] of Object.entries(listeners)) { - if (!(key in ipcListeners)) { - ipcListeners[key] = listener; - listener.listen(); - } - } -}; - -export default function initIpcMain(): void { - _initHandlers(); - _initListeners(); -} - - diff --git a/src/ipc/listeners.ts b/src/ipc/listeners.ts deleted file mode 100644 index 8ef5d45..0000000 --- a/src/ipc/listeners.ts +++ /dev/null @@ -1,24 +0,0 @@ - -import { IpcListener } from "@/composables/useIpcMain" -import { sendMessage } from '@/session'; -import { collect } from "@/audio"; -import { updateAppState } from "@/account"; - -const CLIENT_MESSAGE_CHANNEL = "post-session-send" -const GET_AUDIO_CHANNEL = "post-audio-collect"; -const APP_MOUNT_CHANNEL = "post-app-mount"; - -export const messageListener = new IpcListener({ - 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 new file mode 100644 index 0000000..38967ea --- /dev/null +++ b/src/main.js @@ -0,0 +1,35 @@ +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(); + + /* Check for an update every minunte */ + setInterval(autoUpdater.checkForUpdates, 60000); +}); + +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()); + +// Update when a new update is downloaded. +autoUpdater.on("update-downloaded", update); + diff --git a/src/main.ts b/src/main.ts deleted file mode 100644 index a360835..0000000 --- a/src/main.ts +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Where the background logic really begins, gets called by app.onReady(). - * - * Handles authentication. If profile is set, we launch a session, which consis - * of opening a connection with the platform, initializing the audio streams. - * - * The session is primarily an interface between the frontend and the platform, - * relaying messages from one to the other. - * - */ - -import initIpcMain from "@/ipc/index"; -import { accountAuth, updateAppState } from "./account"; -import { launchSession } from "./session"; -import createWindow from "./window"; - -let authState: AuthState | null; - -export default async function main() { - - /* initiate controls for frontend to use when needed */ - initIpcMain(); - - /* launch browser window */ - await createWindow(); - - try { - authState = await accountAuth() as AuthState; - } catch(e) { - console.log('AUTH:', e); - authState = null; - } finally { - if (authState) { - launchSession(authState.token as string); - } - updateAppState(); - } - -} diff --git a/src/render/App.vue b/src/render/App.vue deleted file mode 100644 index 686591b..0000000 --- a/src/render/App.vue +++ /dev/null @@ -1,76 +0,0 @@ - - - - - diff --git a/src/render/assets/connectLogo.svg b/src/render/assets/connectLogo.svg new file mode 100644 index 0000000..7adceea --- /dev/null +++ b/src/render/assets/connectLogo.svg @@ -0,0 +1,12 @@ + diff --git a/src/render/assets/fonts/SF-Compact-Display-Bold.otf b/src/render/assets/fonts/SF-Compact-Display-Bold.otf new file mode 100755 index 0000000..409e11a Binary files /dev/null and b/src/render/assets/fonts/SF-Compact-Display-Bold.otf differ diff --git a/src/render/assets/fonts/SF-Compact-Rounded-Bold.otf b/src/render/assets/fonts/SF-Compact-Rounded-Bold.otf new file mode 100755 index 0000000..04cf9fc Binary files /dev/null and b/src/render/assets/fonts/SF-Compact-Rounded-Bold.otf differ diff --git a/src/render/assets/fonts/SF-Pro-Text-Regular.otf b/src/render/assets/fonts/SF-Pro-Text-Regular.otf new file mode 100755 index 0000000..06dbe85 Binary files /dev/null and b/src/render/assets/fonts/SF-Pro-Text-Regular.otf differ diff --git a/src/render/assets/settingsIcon.svg b/src/render/assets/settingsIcon.svg new file mode 100644 index 0000000..25399f5 --- /dev/null +++ b/src/render/assets/settingsIcon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/render/components/app.js b/src/render/components/app.js new file mode 100644 index 0000000..1793563 --- /dev/null +++ b/src/render/components/app.js @@ -0,0 +1,44 @@ +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 new file mode 100644 index 0000000..a42364c --- /dev/null +++ b/src/render/components/bubble.js @@ -0,0 +1,48 @@ +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 deleted file mode 100644 index 982cef3..0000000 --- a/src/render/components/bubble.vue +++ /dev/null @@ -1,331 +0,0 @@ - - - - - - - diff --git a/src/render/components/context.js b/src/render/components/context.js new file mode 100644 index 0000000..0c9e89c --- /dev/null +++ b/src/render/components/context.js @@ -0,0 +1,33 @@ + +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 new file mode 100644 index 0000000..23d2683 --- /dev/null +++ b/src/render/components/control/draggify.js @@ -0,0 +1,195 @@ +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 new file mode 100644 index 0000000..ac9c790 --- /dev/null +++ b/src/render/components/control/drop.js @@ -0,0 +1,24 @@ +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 new file mode 100644 index 0000000..3e9a0bf --- /dev/null +++ b/src/render/components/control/scroll.js @@ -0,0 +1,20 @@ +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 new file mode 100644 index 0000000..cd18db9 --- /dev/null +++ b/src/render/components/control/text.js @@ -0,0 +1,84 @@ +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/control/valid.js b/src/render/components/control/valid.js new file mode 100644 index 0000000..dbb710e --- /dev/null +++ b/src/render/components/control/valid.js @@ -0,0 +1,23 @@ +export const isEmail = (value) => { + console.log(value); + if (!/(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/.test(value)) { + return "Not a valid email." + } +} + +export const isPassword = (value) => { + if (!/^(?=.*[0-9])(?=.*[!@#$%^&*])[a-zA-Z0-9!@#$%^&*]{6,16}$/.test(value)) { + return "Password must have symbol, number, uppercase and be 6-16 length."; + } +} + +/** + * Name is optional and therefore doesn't return error if no value. + */ +export const isName = (value) => { + if (value) { + if (!/^([a-zA-Z ]){5,30}$/.test(value)) { + return "Name must be at least 5 long."; + } + } +} \ No newline at end of file diff --git a/src/render/components/controllers/bubble.control.ts b/src/render/components/controllers/bubble.control.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/render/components/controllers/helpers.ts b/src/render/components/controllers/helpers.ts deleted file mode 100644 index 23976a8..0000000 --- a/src/render/components/controllers/helpers.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { ref } from "vue"; -import anime from "animejs"; -import { v4 as uuidv4 } from 'uuid'; - - -export function animateTextInput () { - - const side = ref("right"); - - function show () { - const t1 = (side.value === "right") ? 50 : -70; - const t2 = (side.value === "right") ? 110 : -130; - - anime({ - targets: '#textInput', - opacity: [0, 1], - translateX: [t1, t2], - scale: [0.3, 1], - duration: 500, - easing: 'easeOutExpo', - }) - - } - - function hide () { - const t = (side.value === "right") ? 50 : -80; - - anime({ - targets: '#textInput', - opacity: [1, 0], - translateX: t, - scale: 0.3, - duration: 500, - easing: 'easeOutExpo', - }) - - } - - function switchSide () { - const t = (side.value === "right") ? -130 : 110; - - anime({ - targets: '#textInput', - translateX: t, - duration: 500, - easing: 'easeOutExpo', - }) - - } - - return { - side, - show, - hide, - switchSide - }; - -} - -export function animateAudioInput () { - - function show () { - anime({ - targets: '#recIcon', - opacity: [0, 0.75], - scale: [0.0, 1], - duration: 250, - easing: 'linear', - }) - } - - function hide () { - anime({ - targets: '#recIcon', - opacity: [0.75, 0], - scale: [1, 0], - duration: 250, - easing: 'linear', - }) - } - - return { - show, - hide - }; - -} - - -export function newMessage ({ - text=false, - audio=false, - context=false, - uid=uuidv4() -}) { - return { - text: text, - audio: audio, - context: context, - uid: uid - }; -} diff --git a/src/render/components/controllers/inputItem.control.audio.ts b/src/render/components/controllers/inputItem.control.audio.ts deleted file mode 100644 index facf30c..0000000 --- a/src/render/components/controllers/inputItem.control.audio.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { onMounted, onUnmounted, ref, Ref } from "vue"; -import { postMessage } from "@/render/ipc"; -import { newMessage, animateAudioInput } from "./helpers"; -import { invokeReturnAudio, postAudioChunk } from "@/render/ipc"; - -export default function useAudioInputController (typing: Ref) { - - const recording = ref(false); - let mediaRecorder: MediaRecorder; - - const { show, hide } = animateAudioInput(); - - // initialize audio - const conf = {audio: true, video: false} - navigator.mediaDevices.getUserMedia(conf).then((stream: MediaStream) => { - - const options = {mimeType: 'audio/webm'}; - mediaRecorder = new MediaRecorder(stream, options); - - // post any mew audio to backend - mediaRecorder.addEventListener('dataavailable', (e: BlobEvent) => { - e.data.arrayBuffer().then((buff: ArrayBuffer) => { - postAudioChunk(buff); - }); - }); - - // get audio and post new message to backend - mediaRecorder.addEventListener('stop', (_e: Event) => { - invokeReturnAudio().then((audio: ArrayBuffer[] | Error) => { - console.log(audio); - // postMessage(newMessage({audio: audio})); - }); - }); - - }); - - // start recording on space bar - const record = () => { - console.log("INPT:Capturing audio...") - mediaRecorder.start(); - recording.value = true; - show() - } - - // stop recording and send on release - const stop = () => { - console.log("INPT:Stopping record.") - mediaRecorder.stop(); - recording.value = false; - hide(); - } - - const onKeyDown = (e: KeyboardEvent) => { - if (e.keyCode == 32 && !typing.value) record(); - } - - const onKeyUp = (e: KeyboardEvent) => { - if (e.keyCode == 32 && recording.value) stop(); - } - - //----------------------------------------------------------- - - onMounted(() => { - window.addEventListener("keydown", onKeyDown); - window.addEventListener("keyup", onKeyUp); - }); - - onUnmounted(() => { - window.removeEventListener("keydown", onKeyDown); - window.removeEventListener("keyup", onKeyUp); - }) - - return { - recording - } - -} diff --git a/src/render/components/controllers/inputItem.control.text.ts b/src/render/components/controllers/inputItem.control.text.ts deleted file mode 100644 index 941ae89..0000000 --- a/src/render/components/controllers/inputItem.control.text.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { Ref, ref, watch, onMounted, onUnmounted } from "vue"; -import { postMessage } from "@/render/ipc"; -import { newMessage, animateTextInput } from "./helpers"; - - -export default function useTextInputController(elementX: Ref) { - - let textInput: HTMLInputElement | null; - - const { side, show, hide, switchSide } = animateTextInput(); - - let firstKey = true; - const typing = ref(false); - - // Prep inputItem for typing. - const prepInput = () => { - show() - typing.value = true - } - - // Clear and hide inputItem after done typing. - const clearInput = () => { - - if (textInput) { - textInput.value = ""; - textInput.blur(); - } - - hide() - firstKey = true; - typing.value = false; - } - - // Send a message and clean up after. - const sendMessage = () => { - if (textInput) { - - // Send it to the backend for processing. - const message = newMessage({ - text: false - }); - - // postMessage(message); - - clearInput() - } - } - - // Keys that are capable of opening the text input (numbers and letters). - const isHotKey = (key: number) => { - if (key >= 47 && key <= 91) { // a letter - return true - } - } - - //---Callbacks----------------------------------------------- - - const onKeyDown = (e: KeyboardEvent) => { - const key = e.keyCode; - - if (textInput) { - - // Only runs on firstKey. - if (firstKey) { - - if (!isHotKey(key)) { - return - } - - prepInput() - } - - textInput.focus(); - - // Close input when no text or on ESC. - if ((textInput.value == "") && (!firstKey) && (key === 8)) { // backspace - clearInput() - return - } - - if (key === 27) { // escape - clearInput() - return - } - - // Close and send on enter. - if (key === 13) { - if (textInput.value) { - sendMessage() - return - } - } - - if (firstKey) firstKey = false - } - } - - //----------------------------------------------------------- - - // Watch parent position and update side. - watch(elementX, (elementX, _previous) => { - const winW = window.innerWidth - - // Logic depends on the side we are on. - if (side.value === "right") { - if (winW - elementX < 230) { - switchSide() - side.value = "left" - } - } - - else { - if (winW - elementX > 230) { - switchSide() - side.value = "right" - } - } - - }); - - onMounted(() => { - textInput = document.getElementById("textInput") as HTMLInputElement; - window.addEventListener("keydown", onKeyDown); - }) - - onUnmounted(() => { - window.removeEventListener("keydown", onKeyDown); - }); - - return { - typing - } - -} diff --git a/src/render/components/controllers/messenger.control.ts b/src/render/components/controllers/messenger.control.ts deleted file mode 100644 index b9c3a96..0000000 --- a/src/render/components/controllers/messenger.control.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { ref } from 'vue'; -import useScroll from "@/render/composables/useScroll"; - -const messagesRef = ref(); - -/* seed the canvas with messages */ -const seedCanvas = (messages: Message[]) => { - messagesRef.value = messages; -} - -const addMessage = (message: Message) => { - messagesRef.value.push(message); -} - -const updateMessage = (message: Message) => { - - let target_message = messagesRef.value.filter((m: Message) => { - return m.uid = message.uid; - })[0]; - - if (target_message) { - target_message = message; - } - -} - -export default function useMessages() { - - const { updateScrollRef, adjustScroll } = useScroll("messenger"); - - return { - messagesRef, - seedCanvas, - addMessage, - updateMessage - }; - -} diff --git a/src/render/components/form.js b/src/render/components/form.js new file mode 100644 index 0000000..ecc02f3 --- /dev/null +++ b/src/render/components/form.js @@ -0,0 +1,86 @@ + +// Basic debounce implentation +const debounce = (fn, delay) => { + let timeoutId; + return (...args) => { + if (timeoutId) clearTimeout(timeoutId); + timeoutId = setTimeout(() => { + fn(...args); + }, delay); + } +} + +/** + * Dynamic form component with auto submission and validation. + * + * props + * .fields => List of field objects + * .channel => e.g. "/login" + * .modifier => Form description + */ +const SmartForm = +{ + props: ["fields", "channel", "modifier"], + + setup(props) + { + // Error log below form + const log = Vue.ref(props.modifier); + + /** + * Valid and submit form data to channel. + */ + const submit = async () => { + + // Validate each field + for (let field of props.fields) { + const error = field.valid(field.value); + if (error) return error; + } + + // Reduce fields into the form + const form = props.fields.reduce((v, n) => { + v[n.name] = n.value; + return v; + }, {}); + + console.log("Submitting =>", form); + return await window.mainApi.invoke(props.channel, form); + } + + /** + * Wrapper for submit that is debounced and updates UI elements + */ + const preSub = debounce(async () => { + + const error = await submit(); + + log.value = error ? error : props.modifier + + }, 2000); + + return { preSub, log }; + }, + + template: ` +
+ +
+ + + +
+ +
{{ log }}
+ +
` +} + +export default SmartForm; + + + diff --git a/src/render/components/header.js b/src/render/components/header.js new file mode 100644 index 0000000..5aee0de --- /dev/null +++ b/src/render/components/header.js @@ -0,0 +1,23 @@ +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 deleted file mode 100644 index cf881c3..0000000 --- a/src/render/components/settings.vue +++ /dev/null @@ -1,164 +0,0 @@ - - - - - - diff --git a/src/render/components/splash.js b/src/render/components/splash.js new file mode 100644 index 0000000..220eaad --- /dev/null +++ b/src/render/components/splash.js @@ -0,0 +1,50 @@ +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 deleted file mode 100644 index 247847a..0000000 --- a/src/render/components/splash.vue +++ /dev/null @@ -1,69 +0,0 @@ - - - - - diff --git a/src/render/components/ws.js b/src/render/components/ws.js new file mode 100644 index 0000000..265915b --- /dev/null +++ b/src/render/components/ws.js @@ -0,0 +1,35 @@ + +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 deleted file mode 100644 index f3d8978..0000000 --- a/src/render/composables/useDraggify.ts +++ /dev/null @@ -1,199 +0,0 @@ -import { onMounted, ref, onUnmounted } from "vue"; - -//---Dragabble Helper Funcs-------------------------------------- - -// Calculate distance to nearest side. -function calcSideProximity (elementX: number, winW: number) { - - // Calc right short. - let short = winW - elementX - 40; - - // See if it's left short. - if (elementX + 20 < winW / 2) { - short = elementX - } - - return short -} - -// Update elementX or elementY value on window resize -function calcPosition (elementPosition: number, percent: number, - short: number, win: number) { - - // Is it close to the right/bottom side? - if (percent > 0.75) { - elementPosition = win - short - 40; - } - - // Is it not close to a side? - if (percent < 0.75 && percent > 0.25) { - elementPosition = win * percent - } - - return elementPosition -} - -export default function draggify(elementId: string, xStart: number, - yStart: number, margin: number) { - - let element: HTMLElement | null; - - // Cords of inputItem. - const elementX = ref(0); - const elementY = ref(0); - - // Position of inputItem on terms of percentage of window. - let percentX: number; - let percentY: number; - - // How close inputItem is to closest X or Y side. - let xShort: number; - let yShort: number; - - // Keep track of window size; - let winW = window.innerWidth; - let winH = window.innerHeight; - - //---Reposition Anime----------------------------------------------- - - // Move element to target smoothly. - const repositionAnime = (xChange: number, yChange: number) => { - const xStep = xChange / 6000; - const yStep = yChange / 6000; - - for (let i = 1; i <= 6000; i++) { - - setTimeout(() => { - elementX.value += xStep; - elementY.value += yStep; - }, 16) // 60fps - - } - } - - //---Event Handlers----------------------------------------------- - - // Update the position of inputItem on mouse dragging. - const onMouseMove = (e: any) => { - e.preventDefault() - element = document.getElementById(elementId); - - if (element) { - const inputItemRect = element.getBoundingClientRect(); - elementX.value = inputItemRect.left + e.movementX; - elementY.value = inputItemRect.top + e.movementY; - } - - } - - // Add an event listener for dragging. - const onMouseDown = (_e: any) => { - _e.preventDefault() - window.addEventListener('mousemove', onMouseMove, true); - } - - // Update position references when user is done moving targetEl. - const onMouseUp = (_e: any) => { - window.removeEventListener('mousemove', onMouseMove, true); - - // See if and calculate reposition. - let x = 0; // vector change - let y = 0; - - if (elementX.value < 0) { - x = (elementX.value - margin)*-1 - } - - if (elementX.value > winW - 40) { - const b = winW - 40 - margin; - x = (elementX.value - b)*-1 - } - - // Reposition y - if (elementY.value < 0) { - y = (elementY.value - margin)*-1 - } - - if (elementY.value > winH - 40) { - const d = winH - 40 - margin; - y = (elementY.value - d)*-1 - } - - // Reposition if needed. - if (x !== 0 || y !== 0) repositionAnime(x, y); - - xShort = calcSideProximity(elementX.value, winW); - yShort = calcSideProximity(elementY.value, winH); - - // Update percentages. - percentX = elementX.value / winW; - percentY = elementY.value / winH; - - // Save position. - const position = { - x: elementX.value, - y: elementY.value - } - window.localStorage.setItem("inputItem_position", JSON.stringify(position)); - } - - // Update position of targetEl on windowResize. - const onWindowResize = (_e: any) => { - - // Update window dimensions. - winW = window.innerWidth; - winH = window.innerHeight; - - elementX.value = calcPosition(elementX.value, percentX, xShort, winW); - elementY.value = calcPosition(elementY.value, percentY, yShort, winH); - - } - - //--------------------------------------------------------------- - - onMounted(() => { - element = document.getElementById(elementId); - - if (element) { - element.addEventListener('mousedown', onMouseDown, false); - } - - window.addEventListener("mouseup", onMouseUp, false); - - // Initialize the positional references. - percentX = elementX.value / window.innerWidth; - percentY = elementY.value / window.innerHeight; - - xShort = calcSideProximity(elementX.value, window.innerWidth); - yShort = calcSideProximity(elementX.value, window.innerHeight); - - // Then, we can listen for window resize. - window.addEventListener('resize', onWindowResize, false); - }); - - // Try loading initPosition, otherwise set default values - let initPosition: any; - const rawData = window.localStorage.getItem("inputItem_position") - - if (rawData) { - initPosition = JSON.parse(rawData) - } else { - initPosition = {x: xStart, y: yStart} - } - - elementX.value = initPosition.x; - elementY.value = initPosition.y; - - // remove event listeners on component dismount. - onUnmounted(() => { - window.removeEventListener('resize', onWindowResize); - window.removeEventListener('mouseup', onMouseUp); - if (element) element.removeEventListener('mousedown', onMouseDown); - }) - - return { - elementX, - elementY - } - -} diff --git a/src/render/composables/useIpcRend.ts b/src/render/composables/useIpcRend.ts deleted file mode 100644 index 9ba7f73..0000000 --- a/src/render/composables/useIpcRend.ts +++ /dev/null @@ -1,54 +0,0 @@ - -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 deleted file mode 100644 index f731584..0000000 --- a/src/render/composables/useMessages.ts +++ /dev/null @@ -1,33 +0,0 @@ -// 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 deleted file mode 100644 index 3575de7..0000000 --- a/src/render/composables/useProfile.ts +++ /dev/null @@ -1,27 +0,0 @@ -// 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 deleted file mode 100644 index 811e7fc..0000000 --- a/src/render/composables/useScroll.ts +++ /dev/null @@ -1,29 +0,0 @@ - - -export default function useScroll(element: string) { - - let isScrolledToBottom: boolean; - const view = document.getElementById(element) - - // Update isScrolledToBottom - const updateScrollRef = () => { - if (view) isScrolledToBottom = view.scrollHeight - view.clientHeight <= view.scrollTop + 1; - return isScrolledToBottom; - } - - // Adjust scroll after we add content to the messenger. - const adjustScroll = () => { - if (view) { - view.scrollTo({ - top: view.scrollHeight - view.clientHeight, - behavior: 'smooth' - }); - } - } - - return { - updateScrollRef, - adjustScroll, - }; - -} \ No newline at end of file diff --git a/src/render/index.html b/src/render/index.html new file mode 100644 index 0000000..e45bb6c --- /dev/null +++ b/src/render/index.html @@ -0,0 +1,17 @@ + + + + + + + + +
+ + + + + + + + \ No newline at end of file diff --git a/src/render/index.js b/src/render/index.js new file mode 100644 index 0000000..d7df8b0 --- /dev/null +++ b/src/render/index.js @@ -0,0 +1,5 @@ +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 deleted file mode 100644 index bc68049..0000000 --- a/src/render/ipc.ts +++ /dev/null @@ -1,80 +0,0 @@ - -import useIpc from "@/render/composables/useIpcRend"; -import * as rendererListeners from "./listeners"; - -const { post, invoke } = useIpc(); - -/** - * - * Account and auth related endpoints - * - */ - - -export const invokeLogin = async ( - payload: LoginPayload -): Promise => ( - 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 deleted file mode 100644 index 876d206..0000000 --- a/src/render/listeners.ts +++ /dev/null @@ -1,32 +0,0 @@ - -import { IpcRendererListener } from "./composables/useIpcRend" -import { setProfile } from "./composables/useProfile"; -import { setMessages, addMessage, updateMessage } from "./composables/useMessages"; - -const SET_PROFILE_CHANNEL = "set-profile"; - -const INIT_MESSAGES_CHANNEL = "init-messages"; -const ADD_MESSAGE_CHANNEL = "add-message"; -const UPDATE_MESSAGE_CHANNEL = "update-message"; - -export const setProfileListener = new IpcRendererListener({ - channel: SET_PROFILE_CHANNEL, - listenerCallback: setProfile -}); - -export const initMessagesListener = new IpcRendererListener({ - channel: INIT_MESSAGES_CHANNEL, - listenerCallback: setMessages -}); - - -export const addMessagesListener = new IpcRendererListener({ - channel: ADD_MESSAGE_CHANNEL, - listenerCallback: addMessage -}); - -export const updateMessagesListener = new IpcRendererListener({ - channel: UPDATE_MESSAGE_CHANNEL, - listenerCallback: updateMessage -}); - diff --git a/src/render/main.css b/src/render/main.css new file mode 100644 index 0000000..cd26154 --- /dev/null +++ b/src/render/main.css @@ -0,0 +1,582 @@ + +@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 deleted file mode 100644 index 5375889..0000000 --- a/src/render/main.ts +++ /dev/null @@ -1,21 +0,0 @@ - -// src/main.ts - -import App from "./App.vue"; - -import mitt from "mitt"; -import { createApp } from "vue"; - -import { initIpcRendererListeners } from "./ipc" - - -// Handle ipcMain events. -initIpcRendererListeners(); - -// Handle events. -const emitter = mitt(); - -const app = createApp(App); - -app.provide("mitt", emitter); -app.mount("#app"); diff --git a/src/render/preload.js b/src/render/preload.js new file mode 100644 index 0000000..adef673 --- /dev/null +++ b/src/render/preload.js @@ -0,0 +1,53 @@ +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 deleted file mode 100644 index 054f672..0000000 --- a/src/render/preload.ts +++ /dev/null @@ -1,38 +0,0 @@ -// All of the Node.js APIs are available in the preload process. -// It has the same sandbox as a Chrome extension. -import { ipcRenderer } from "electron"; - -declare global { - interface Window { - ipcRenderer: typeof ipcRenderer; - } -} - -window.ipcRenderer = ipcRenderer; - -process.once("loaded", () => { - - window.addEventListener("message", event => { - - // do something with custom event - const message = event.data; - - if (message.endpoint === "update-menu-bar") { - ipcRenderer.send("update-menu-bar", message.content); - } - - if (message.endpoint === "update-recorder") { - ipcRenderer.send("update-recorder", message.content); - } - - if (message.endpoint === "client-message") { - ipcRenderer.send("client-message", message.content); - } - - if (message.endpoint === "logout") { - ipcRenderer.send("logout", message.content); - } - - }); - -}); diff --git a/src/render/shims-vue.d.ts b/src/render/shims-vue.d.ts deleted file mode 100644 index 35a61a9..0000000 --- a/src/render/shims-vue.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -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 new file mode 100644 index 0000000..6e5ee00 --- /dev/null +++ b/src/render/vendor/vue.js @@ -0,0 +1,15872 @@ +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. `