diff --git a/DeskThingServer/package-lock.json b/DeskThingServer/package-lock.json index 9cb6be01..45b03187 100644 --- a/DeskThingServer/package-lock.json +++ b/DeskThingServer/package-lock.json @@ -1,12 +1,12 @@ { "name": "deskthing", - "version": "0.9.0", + "version": "0.9.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "deskthing", - "version": "0.9.0", + "version": "0.9.3", "hasInstallScript": true, "dependencies": { "@electron-toolkit/preload": "^3.0.0", @@ -22,6 +22,7 @@ "react-qr-code": "^2.0.15", "react-rewards": "^2.0.4", "react-router-dom": "^6.26.2", + "react-select": "^5.8.3", "uuid": "^10.0.0", "ws": "^8.17.1", "zustand": "^5.0.0-rc.2" @@ -81,7 +82,6 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/highlight": "^7.24.7", @@ -146,7 +146,6 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.7.tgz", "integrity": "sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.24.7", @@ -189,7 +188,6 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==", - "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.24.7" @@ -202,7 +200,6 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz", "integrity": "sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.24.7", @@ -216,7 +213,6 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz", "integrity": "sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==", - "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.24.7" @@ -229,7 +225,6 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/traverse": "^7.24.7", @@ -287,7 +282,6 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.24.7" @@ -300,7 +294,6 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz", "integrity": "sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -310,7 +303,6 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -344,7 +336,6 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.24.7", @@ -360,7 +351,6 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz", "integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==", - "dev": true, "license": "MIT", "bin": { "parser": "bin/babel-parser.js" @@ -417,11 +407,21 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz", + "integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz", "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==", - "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.24.7", @@ -436,7 +436,6 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.7.tgz", "integrity": "sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.24.7", @@ -458,7 +457,6 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.7.tgz", "integrity": "sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.24.7", @@ -833,6 +831,133 @@ "node": ">= 10.0.0" } }, + "node_modules/@emotion/babel-plugin": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.12.0.tgz", + "integrity": "sha512-y2WQb+oP8Jqvvclh8Q55gLUyb7UFvgv7eJfsj7td5TToBrIUtPay2kMrZi4xjq9qw2vD0ZR5fSho0yqoFgX7Rw==", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/serialize": "^1.2.0", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/babel-plugin/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" + }, + "node_modules/@emotion/babel-plugin/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@emotion/babel-plugin/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@emotion/cache": { + "version": "11.13.1", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.13.1.tgz", + "integrity": "sha512-iqouYkuEblRcXmylXIwwOodiEK5Ifl7JcX7o6V4jI3iW4mLXX3dmt5xwBtIkJiQEXFAI+pC8X0i67yiPkH9Ucw==", + "dependencies": { + "@emotion/memoize": "^0.9.0", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.0", + "@emotion/weak-memoize": "^0.4.0", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==" + }, + "node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==" + }, + "node_modules/@emotion/react": { + "version": "11.13.3", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.13.3.tgz", + "integrity": "sha512-lIsdU6JNrmYfJ5EbUCf4xW1ovy5wKQ2CkPRM4xogziOxH1nXxBSjpC9YqbFAP7circxMfYp+6x676BqWcEiixg==", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.12.0", + "@emotion/cache": "^11.13.0", + "@emotion/serialize": "^1.3.1", + "@emotion/use-insertion-effect-with-fallbacks": "^1.1.0", + "@emotion/utils": "^1.4.0", + "@emotion/weak-memoize": "^0.4.0", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.2.tgz", + "integrity": "sha512-grVnMvVPK9yUVE6rkKfAJlYZgo0cu3l9iMC77V7DW6E1DUIrU68pSEXRmFZFOFB1QFo57TncmOcvcbMDWsL4yA==", + "dependencies": { + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/unitless": "^0.10.0", + "@emotion/utils": "^1.4.1", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/sheet": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==" + }, + "node_modules/@emotion/unitless": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==" + }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.1.0.tgz", + "integrity": "sha512-+wBOcIV5snwGgI2ya3u99D7/FJquOIniQT1IKyDsBmEgwvpxMNeS65Oib7OnE2d2aY+3BU4OiH+0Wchf8yk3Hw==", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.1.tgz", + "integrity": "sha512-BymCXzCG3r72VKJxaYVwOXATqXIZ85cuvg0YOUDxMGNrKc1DJRZk8MgV5wyXRyEayIMd4FuXJIUgTBXvDNW5cA==" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", @@ -1337,6 +1462,28 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.6.8", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.8.tgz", + "integrity": "sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA==", + "dependencies": { + "@floating-ui/utils": "^0.2.8" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.12", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.12.tgz", + "integrity": "sha512-NP83c0HjokcGVEMeoStg317VD9W7eDlGK7457dMBANbKA6GJZdc7rjujdgqzTaz93jkGgc5P/jeWbaCHnMNc+w==", + "dependencies": { + "@floating-ui/core": "^1.6.0", + "@floating-ui/utils": "^0.2.8" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.8.tgz", + "integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==" + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", @@ -1506,7 +1653,6 @@ "version": "0.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/set-array": "^1.2.1", @@ -1521,7 +1667,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -1531,7 +1676,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -1541,14 +1685,12 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.25", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -2171,6 +2313,11 @@ "undici-types": "~5.26.4" } }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==" + }, "node_modules/@types/plist": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.5.tgz", @@ -2187,14 +2334,12 @@ "version": "15.7.12", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==", - "devOptional": true, "license": "MIT" }, "node_modules/@types/react": { "version": "18.3.3", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz", "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==", - "devOptional": true, "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -2211,6 +2356,14 @@ "@types/react": "*" } }, + "node_modules/@types/react-transition-group": { + "version": "4.4.11", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.11.tgz", + "integrity": "sha512-RM05tAniPZ5DZPzzNFP+DmrcOdD0efDUxMy3145oljWSl3x9ZV5vhme98gTxFrj2lhXvmGNnUiuDyJgY9IKkNA==", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/responselike": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", @@ -2595,7 +2748,6 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^1.9.0" @@ -3135,6 +3287,59 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, + "node_modules/babel-plugin-macros/node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/babel-plugin-macros/node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/babel-plugin-macros/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "engines": { + "node": ">= 6" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -3600,7 +3805,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -3652,7 +3856,6 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^3.2.1", @@ -3806,7 +4009,6 @@ "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "1.1.3" @@ -3816,7 +4018,6 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true, "license": "MIT" }, "node_modules/combined-stream": { @@ -4113,7 +4314,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "devOptional": true, "license": "MIT" }, "node_modules/data-view-buffer": { @@ -4487,6 +4687,15 @@ "node": ">=6.0.0" } }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, "node_modules/dotenv": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-9.0.2.tgz", @@ -5038,7 +5247,6 @@ "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, "license": "MIT", "dependencies": { "is-arrayish": "^0.2.1" @@ -5275,7 +5483,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.8.0" @@ -5955,6 +6162,11 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -6339,7 +6551,6 @@ "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -6448,7 +6659,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -6556,6 +6766,14 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dependencies": { + "react-is": "^16.7.0" + } + }, "node_modules/hosted-git-info": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", @@ -6732,7 +6950,6 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, "license": "MIT", "dependencies": { "parent-module": "^1.0.0", @@ -6818,7 +7035,6 @@ "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true, "license": "MIT" }, "node_modules/is-async-function": { @@ -6910,7 +7126,6 @@ "version": "2.14.0", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.14.0.tgz", "integrity": "sha512-a5dFJih5ZLYlRtDc0dZWP7RiKr6xIKzmn/oAYCDvdLThadVgyJwlaoQPmRtMSpz+rk0OGAgIu+TcM9HUF0fk1A==", - "dev": true, "license": "MIT", "dependencies": { "hasown": "^2.0.2" @@ -7426,7 +7641,6 @@ "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "dev": true, "license": "MIT", "bin": { "jsesc": "bin/jsesc" @@ -7445,7 +7659,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true, "license": "MIT" }, "node_modules/json-schema-traverse": { @@ -7610,7 +7823,6 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, "license": "MIT" }, "node_modules/linkify-it": { @@ -7857,6 +8069,11 @@ "node": ">= 0.6" } }, + "node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==" + }, "node_modules/merge-descriptors": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", @@ -8454,7 +8671,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, "license": "MIT", "dependencies": { "callsites": "^3.0.0" @@ -8467,7 +8683,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.0.0", @@ -8524,7 +8739,6 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, "license": "MIT" }, "node_modules/path-scurry": { @@ -8561,7 +8775,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -8578,7 +8791,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { @@ -9170,6 +9382,41 @@ "react-dom": ">=16.8" } }, + "node_modules/react-select": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/react-select/-/react-select-5.8.3.tgz", + "integrity": "sha512-lVswnIq8/iTj1db7XCG74M/3fbGB6ZaluCzvwPGT5ZOjCdL/k0CLWhEK0vCBLuU5bHTEf6Gj8jtSvi+3v+tO1w==", + "dependencies": { + "@babel/runtime": "^7.12.0", + "@emotion/cache": "^11.4.0", + "@emotion/react": "^11.8.1", + "@floating-ui/dom": "^1.0.1", + "@types/react-transition-group": "^4.4.0", + "memoize-one": "^6.0.0", + "prop-types": "^15.6.0", + "react-transition-group": "^4.3.0", + "use-isomorphic-layout-effect": "^1.1.2" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -9274,6 +9521,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, "node_modules/regex": { "version": "4.3.3", "resolved": "https://registry.npmjs.org/regex/-/regex-4.3.3.tgz", @@ -9339,7 +9591,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -10124,6 +10375,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==" + }, "node_modules/sucrase": { "version": "3.35.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", @@ -10221,7 +10477,6 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, "license": "MIT", "dependencies": { "has-flag": "^3.0.0" @@ -10234,7 +10489,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -10468,7 +10722,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -10885,6 +11138,19 @@ "punycode": "^2.1.0" } }, + "node_modules/use-isomorphic-layout-effect": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz", + "integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/utf8-byte-length": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", diff --git a/DeskThingServer/package.json b/DeskThingServer/package.json index e34ea32a..6c47ffbe 100644 --- a/DeskThingServer/package.json +++ b/DeskThingServer/package.json @@ -1,6 +1,6 @@ { "name": "deskthing", - "version": "0.9.2", + "version": "0.9.3", "description": "A DeskThing server UI to interface with the DeskThing car thing app", "main": "./out/main/index.js", "author": "Riprod", @@ -35,6 +35,7 @@ "react-qr-code": "^2.0.15", "react-rewards": "^2.0.4", "react-router-dom": "^6.26.2", + "react-select": "^5.8.3", "uuid": "^10.0.0", "ws": "^8.17.1", "zustand": "^5.0.0-rc.2" diff --git a/DeskThingServer/src/main/handlers/adbHandler.ts b/DeskThingServer/src/main/handlers/adbHandler.ts index 3fecc4a9..50f957d5 100644 --- a/DeskThingServer/src/main/handlers/adbHandler.ts +++ b/DeskThingServer/src/main/handlers/adbHandler.ts @@ -1,9 +1,9 @@ import path from 'path' import { execFile } from 'child_process' import getPlatform from '../utils/get-platform' -import dataListener, { MESSAGE_TYPES } from '../utils/events' +import loggingStore from '../stores/loggingStore' import settingsStore from '../stores/settingsStore' -import { LoggingData } from '@shared/types' +import { ReplyFn, MESSAGE_TYPES } from '@shared/types' const isDevelopment = process.env.NODE_ENV === 'development' const execPath = isDevelopment @@ -25,45 +25,33 @@ const splitArgs = (str: string): string[] => { return matches } -export const handleAdbCommands = async ( - command: string, - send?: (channel: string, ...args: LoggingData[]) => void -): Promise => { +export const handleAdbCommands = async (command: string, replyFn?: ReplyFn): Promise => { const settings = await settingsStore.getSettings() const useGlobalADB = settings.globalADB === true - dataListener.asyncEmit( - MESSAGE_TYPES.LOGGING, - useGlobalADB ? 'Using Global ADB' : 'Using Local ADB' - ) + loggingStore.log(MESSAGE_TYPES.LOGGING, useGlobalADB ? 'Using Global ADB' : 'Using Local ADB') return new Promise((resolve, reject) => { execFile( useGlobalADB ? 'adb' : adbPath, splitArgs(command), { cwd: execPath }, (error, stdout, stderr) => { - console.log(error, stdout, stderr) if (error) { - if (send) { - send('logging', { + replyFn && + replyFn('logging', { status: false, data: 'Error Encountered!', - final: true, + final: false, error: stderr }) - } - dataListener.asyncEmit( - MESSAGE_TYPES.ERROR, - `ADB Error: ${stderr}, ${command}, ${adbPath}` - ) + loggingStore.log(MESSAGE_TYPES.ERROR, `ADB Error: ${stderr}, ${command}, ${adbPath}`) reject(`ADB Error: ${stderr}, ${command}, ${adbPath}`) } else { - if (send) { - send('logging', { + replyFn && + replyFn('logging', { status: true, data: 'ADB Success!', - final: true + final: false }) - } resolve(stdout) } } diff --git a/DeskThingServer/src/main/handlers/appHandler.ts b/DeskThingServer/src/main/handlers/appHandler.ts index 6df63eca..704871df 100644 --- a/DeskThingServer/src/main/handlers/appHandler.ts +++ b/DeskThingServer/src/main/handlers/appHandler.ts @@ -1,10 +1,16 @@ import path from 'path' -import { AppIPCData, ReplyFn } from '@shared/types/ipcTypes' -import dataListener, { MESSAGE_TYPES } from '../utils/events' +import { + App, + AppDataInterface, + AppReturnData, + AppIPCData, + ReplyFn, + MESSAGE_TYPES +} from '@shared/types' +import loggingStore from '../stores/loggingStore' import { getData, setData } from './dataHandler' import { dialog, BrowserWindow } from 'electron' import { sendMessageToApp, AppHandler } from '../services/apps' -import { App, AppDataInterface, AppReturnData } from '@shared/types' const appStore = AppHandler.getInstance() export const appHandler: Record< @@ -109,7 +115,7 @@ export const appHandler: Record< return { path: filePath, name: path.basename(filePath) } }, 'dev-add-app': async (data, replyFn) => { - dataListener.asyncEmit( + loggingStore.log( MESSAGE_TYPES.ERROR, 'Developer App Not implemented Yet ', data.payload.appPath @@ -118,7 +124,6 @@ export const appHandler: Record< replyFn('logging', { status: true, data: 'Finished', final: true }) }, 'send-to-app': async (data, replyFn) => { - console.log('sending data to app: ', data.payload.app, data) await sendMessageToApp(data.payload.app, data.payload) replyFn('logging', { status: true, data: 'Finished', final: true }) }, @@ -130,16 +135,14 @@ export const appHandler: Record< const getApps = (replyFn: ReplyFn): App[] => { replyFn('logging', { status: true, data: 'Getting data', final: false }) - console.log('Getting app data') const data = appStore.getAllBase() replyFn('logging', { status: true, data: 'Finished', final: true }) replyFn('app-data', { status: true, data: data, final: true }) return data } -const setAppData = async (replyFn, id, data: AppDataInterface): Promise => { - console.log('Saving app data: ', data) - dataListener.asyncEmit(MESSAGE_TYPES.LOGGING, 'SERVER: Saving ' + id + "'s data " + data) +const setAppData = async (replyFn: ReplyFn, id, data: AppDataInterface): Promise => { + loggingStore.log(MESSAGE_TYPES.LOGGING, 'SERVER: Saving ' + id + "'s data " + data) await setData(id, data) replyFn('logging', { status: true, data: 'Finished', final: true }) } @@ -150,7 +153,7 @@ const getAppData = async (replyFn, payload): Promise => replyFn('logging', { status: true, data: 'Finished', final: true }) return data } catch (error) { - dataListener.asyncEmit(MESSAGE_TYPES.ERROR, 'SERVER: Error saving manifest' + error) + loggingStore.log(MESSAGE_TYPES.ERROR, 'SERVER: Error saving manifest' + error) console.error('Error setting client manifest:', error) replyFn('logging', { status: false, data: 'Unfinished', error: error, final: true }) return null diff --git a/DeskThingServer/src/main/handlers/authHandler.ts b/DeskThingServer/src/main/handlers/authHandler.ts index ba64065c..bbd87dfc 100644 --- a/DeskThingServer/src/main/handlers/authHandler.ts +++ b/DeskThingServer/src/main/handlers/authHandler.ts @@ -3,8 +3,8 @@ import { sendMessageToApp } from '../services/apps' // Assuming you have an app import http from 'http' import url from 'url' import settingsStore from '../stores/settingsStore' -import dataListener, { MESSAGE_TYPES } from '../utils/events' -import { Settings } from '@shared/types' +import loggingStore from '../stores/loggingStore' +import { Settings, MESSAGE_TYPES } from '@shared/types' const successView = '

Success

You can now close this window.

' @@ -14,7 +14,7 @@ let callBackPort: number function handleCallback(req: http.IncomingMessage, res: http.ServerResponse): void { const parsedUrl = url.parse(req.url || '', true) - dataListener.asyncEmit( + loggingStore.log( MESSAGE_TYPES.LOGGING, `AUTH: Received callback request for ${parsedUrl.pathname}` ) @@ -29,7 +29,6 @@ function handleCallback(req: http.IncomingMessage, res: http.ServerResponse): vo const appName = urlParts[1] // The app name should be the third part after '/callback/' const config = getAppData() // Assuming getConfig() returns an object with active apps - console.log('AUTH DATA: ', config) if (!config.apps || !config.apps.some((app) => app.name === appName && app.enabled)) { res.writeHead(404, { 'Content-Type': 'text/html' }) res.end(`

App Not Found

App '${appName}' not found or not active.

`) @@ -37,7 +36,6 @@ function handleCallback(req: http.IncomingMessage, res: http.ServerResponse): vo } const code = parsedUrl.query.code as string - console.log('AUTH CODE: ', code) sendMessageToApp(appName, { type: 'callback-data', payload: code }) res.writeHead(200, { 'Content-Type': 'text/html' }) @@ -47,11 +45,10 @@ function handleCallback(req: http.IncomingMessage, res: http.ServerResponse): vo const startServer = async (): Promise => { if (server) { await server.close(() => { - console.log('CALLBACK: Previous server closed.') + loggingStore.log(MESSAGE_TYPES.LOGGING, 'CALLBACK: Shutting down the server...') }) } - console.log('CALLBACK: Starting server...') server = http.createServer((req, res) => { const parsedUrl = new URL(`http://${req.headers.host}${req.url}`) const pathname = parsedUrl.pathname @@ -63,9 +60,8 @@ const startServer = async (): Promise => { } }) - console.log('CALLBACK: Listening...') server.listen(callBackPort, () => { - dataListener.asyncEmit( + loggingStore.log( MESSAGE_TYPES.MESSAGE, `CALLBACK: running at http://localhost:${callBackPort}/` ) @@ -82,16 +78,18 @@ const initializeServer = async (): Promise => { } } -dataListener.on(MESSAGE_TYPES.SETTINGS, (newSettings) => { +settingsStore.addListener((newSettings) => { try { if (newSettings.callbackPort != callBackPort) { callBackPort = newSettings.callbackPort startServer() } else { - dataListener.asyncEmit(MESSAGE_TYPES.LOGGING, 'CALLBACK: Not starting - port is not changed') + loggingStore.log(MESSAGE_TYPES.LOGGING, 'CALLBACK: Not starting - port is not changed') } } catch (error) { - dataListener.asyncEmit(MESSAGE_TYPES.ERROR, 'CALLBACK: Error updating with settings', error) + if (error instanceof Error) { + loggingStore.log(MESSAGE_TYPES.ERROR, 'CALLBACK: Error updating with settings' + error) + } } }) diff --git a/DeskThingServer/src/main/handlers/clientHandler.ts b/DeskThingServer/src/main/handlers/clientHandler.ts index 6e86da2d..8d527b4a 100644 --- a/DeskThingServer/src/main/handlers/clientHandler.ts +++ b/DeskThingServer/src/main/handlers/clientHandler.ts @@ -1,5 +1,5 @@ -import { ClientIPCData, ReplyFn } from '@shared/types/ipcTypes' -import dataListener, { MESSAGE_TYPES } from '../utils/events' +import { ClientIPCData, ClientManifest, SocketData, ReplyFn, MESSAGE_TYPES } from '@shared/types' +import loggingStore from '../stores/loggingStore' import { handleAdbCommands } from './adbHandler' import { configureDevice, @@ -9,7 +9,6 @@ import { HandleWebappZipFromUrl, SetupProxy } from './deviceHandler' -import { ClientManifest, SocketData } from '@shared/types' import { sendMessageToClient, sendMessageToClients } from '../services/client/clientCom' export const clientHandler: Record< @@ -28,7 +27,11 @@ export const clientHandler: Record< return `Pinging ${data.payload}...` } catch (error) { console.error('Error pinging client:', error) - dataListener.asyncEmit(MESSAGE_TYPES.ERROR, error) + if (error instanceof Error) { + loggingStore.log(MESSAGE_TYPES.ERROR, error.message) + } else { + loggingStore.log(MESSAGE_TYPES.ERROR, String(error)) + } return 'Error pinging' + data.payload } }, @@ -45,27 +48,23 @@ export const clientHandler: Record< return handleUrl(data, replyFn) }, adb: async (data, replyFn) => { - console.log('Running ADB command:', data.payload) replyFn('logging', { status: true, data: 'Working', final: false }) - const reply = async (status, data, final, error): Promise => { - replyFn('logging', { - status: status, - data: data, - final: final, - error: error - }) - } - - return await handleAdbCommands(data.payload, reply) + const response = await handleAdbCommands(data.payload, replyFn) + replyFn('logging', { status: true, data: response, final: true }) + return response }, configure: async (data, replyFn) => { replyFn('logging', { status: true, data: 'Configuring Device', final: false }) - return await configureDevice(data.payload, replyFn) + const response = await configureDevice(data.payload, replyFn) + replyFn('logging', { status: true, data: 'Device Configured!', final: true }) + return response }, 'client-manifest': async (data, replyFn) => { if (data.request === 'get') { - return await getClientManifest(false, replyFn) + const response = await getClientManifest(replyFn) + replyFn('logging', { status: true, data: response, final: true }) + return response } else if (data.request === 'set') { return await handleClientManifestUpdate(data.payload, replyFn) } @@ -73,7 +72,7 @@ export const clientHandler: Record< }, 'push-staged': async (data, replyFn) => { try { - console.log('Pushing staged webapp...') + loggingStore.log(MESSAGE_TYPES.LOGGING, 'Pushing staged app...') HandlePushWebApp(data.payload, replyFn) } catch (error) { replyFn('logging', { @@ -82,14 +81,22 @@ export const clientHandler: Record< error: 'Failed to push staged app!', final: true }) - console.error('Error extracting zip file:', error) - dataListener.asyncEmit(MESSAGE_TYPES.ERROR, error) + if (error instanceof Error) { + loggingStore.log(MESSAGE_TYPES.ERROR, error.message) + } else { + loggingStore.log(MESSAGE_TYPES.ERROR, String(error)) + } } }, 'push-proxy-script': async (data, replyFn) => { try { - console.log('Pushing proxy script...') + loggingStore.log(MESSAGE_TYPES.LOGGING, 'Pushing proxy script...') SetupProxy(replyFn, data.payload) + replyFn('logging', { + status: true, + data: 'Proxy script pushed!', + final: true + }) } catch (error) { replyFn('logging', { status: false, @@ -98,7 +105,11 @@ export const clientHandler: Record< final: true }) console.error('Error pushing proxy script:', error) - dataListener.asyncEmit(MESSAGE_TYPES.ERROR, error) + if (error instanceof Error) { + loggingStore.log(MESSAGE_TYPES.ERROR, error.message) + } else { + loggingStore.log(MESSAGE_TYPES.ERROR, String(error)) + } } }, 'run-device-command': async (data, replyFn) => { @@ -110,13 +121,12 @@ export const clientHandler: Record< request: data.payload.request, payload: !payload.includes('{') ? data.payload.payload : JSON.parse(data.payload.payload) } - console.log('Sending data', data) replyFn('logging', { status: true, data: 'Finished', final: true }) return await sendMessageToClients(message) } } -const handleUrl = (data, replyFn: ReplyFn): void => { +const handleUrl = async (data, replyFn: ReplyFn): Promise => { try { replyFn('logging', { status: true, @@ -124,15 +134,24 @@ const handleUrl = (data, replyFn: ReplyFn): void => { final: false }) - const reply = async (channel: string, data): Promise => { - replyFn(channel, data) - } - - HandleWebappZipFromUrl(reply, data.payload) + await HandleWebappZipFromUrl(replyFn, data.payload) + replyFn('logging', { status: true, data: 'Successfully downloaded client', final: true }) } catch (error) { console.error('Error extracting zip file:', error) if (error instanceof Error) { - replyFn('zip-extracted', { status: false, error: error.message, data: null, final: true }) + replyFn('logging', { + status: false, + error: error.message, + data: 'Error handling URL', + final: true + }) + } else { + replyFn('logging', { + status: false, + error: 'Unable to download CLIENT', + data: 'Error handling URL', + final: true + }) } } } diff --git a/DeskThingServer/src/main/handlers/configHandler.ts b/DeskThingServer/src/main/handlers/configHandler.ts index c575f504..bbc54805 100644 --- a/DeskThingServer/src/main/handlers/configHandler.ts +++ b/DeskThingServer/src/main/handlers/configHandler.ts @@ -1,12 +1,11 @@ import { sendIpcData } from '..' -import { AppData, App, Manifest, ButtonMapping } from '@shared/types' -import dataListener, { MESSAGE_TYPES } from '../utils/events' +import { AppData, App, MESSAGE_TYPES, Manifest, ButtonMapping } from '@shared/types' +import loggingStore from '../stores/loggingStore' import { readFromFile, writeToFile } from '../utils/fileHandler' const defaultData: AppData = { apps: [], config: { - audiosources: ['local'], testData: 'thisisastring' } } @@ -18,7 +17,6 @@ const readData = (): AppData => { const data = readFromFile(dataFilePath) if (!data) { // File does not exist, create it with default data - console.log('File does not exist, creating it with default data') writeToFile(defaultData, dataFilePath) return defaultData } @@ -35,11 +33,11 @@ const writeData = (data: AppData): void => { try { const result = writeToFile(data, 'apps.json') if (!result) { - dataListener.asyncEmit(MESSAGE_TYPES.ERROR, 'Error writing data') + loggingStore.log(MESSAGE_TYPES.ERROR, 'Error writing data') } sendIpcData('app-data', data) // Send data to the web UI } catch (err) { - dataListener.asyncEmit(MESSAGE_TYPES.ERROR, 'Error writing data' + err) + loggingStore.log(MESSAGE_TYPES.ERROR, 'Error writing data' + err) console.error('Error writing data:', err) } } @@ -74,11 +72,6 @@ const addAppManifest = (manifest: Manifest, appName: string): void => { // Find existing app by name const existingAppIndex = data.apps.findIndex((app: App) => app.name === appName) - if (manifest.isAudioSource) { - addConfig('audiosources', appName, data) - console.log(appName, 'added to audiosources') - } - if (existingAppIndex !== -1) { // Update existing app data.apps[existingAppIndex].manifest = manifest @@ -91,9 +84,7 @@ const addAppManifest = (manifest: Manifest, appName: string): void => { const addConfig = (configName: string, config: string | Array, data = readData()): void => { if (!data.config) { - const val = { - audiosources: ['local'] - } + const val = {} val[configName] = config data.config = val } else if (Array.isArray(data.config[configName])) { @@ -112,12 +103,11 @@ const addConfig = (configName: string, config: string | Array, data = re } else { data.config[configName] = config } - console.log('THIS IS THE FIRST TIME THIS IS BEING EMITTED - TRY AND TRACK IT') - dataListener.asyncEmit(MESSAGE_TYPES.CONFIG, { - app: 'server', - type: 'config', - payload: data.config - }) + // loggingStore.log(MESSAGE_TYPES.CONFIG, { + // app: 'server', + // type: 'config', + // payload: data.config + // }) writeData(data) } const getConfig = ( @@ -126,9 +116,7 @@ const getConfig = ( const data = readData() if (!data.config) { - const val = { - audiosources: ['local'] - } + const val = {} data.config = val writeData(data) } @@ -160,24 +148,19 @@ const getAppByIndex = (index: number): App | undefined => { } const purgeAppConfig = async (appName: string): Promise => { - console.log('SERVER: Deleting App From Config...', appName) + loggingStore.log(MESSAGE_TYPES.LOGGING, `Purging app: ${appName}`) const data = readData() // Filter out the app to be purged const filteredApps = data.apps.filter((app: App) => app.name !== appName) data.apps = filteredApps - if (Array.isArray(data.config.audiosources)) { - const updatedAudiosources = data.config.audiosources.filter((source) => source !== appName) - data.config.audiosources = updatedAudiosources - } - writeData(data) - dataListener.asyncEmit(MESSAGE_TYPES.CONFIG, { - app: 'server', - type: 'config', - payload: data.config - }) + // loggingStore.log(MESSAGE_TYPES.CONFIG, { + // app: 'server', + // type: 'config', + // payload: data.config + // }) } export { diff --git a/DeskThingServer/src/main/handlers/dataHandler.ts b/DeskThingServer/src/main/handlers/dataHandler.ts index ff8fec17..b11a2945 100644 --- a/DeskThingServer/src/main/handlers/dataHandler.ts +++ b/DeskThingServer/src/main/handlers/dataHandler.ts @@ -62,7 +62,6 @@ const getData = (app: string): AppDataInterface => { } const purgeAppData = async (appName: string): Promise => { - console.log('SERVER: Deleting app data...') const data = readData() delete data[appName] writeData(data) diff --git a/DeskThingServer/src/main/handlers/deviceHandler.ts b/DeskThingServer/src/main/handlers/deviceHandler.ts index 88e800f5..5442ddfb 100644 --- a/DeskThingServer/src/main/handlers/deviceHandler.ts +++ b/DeskThingServer/src/main/handlers/deviceHandler.ts @@ -1,11 +1,12 @@ import { sendIpcData } from '..' -import dataListener, { MESSAGE_TYPES } from '../utils/events' +import loggingStore from '../stores/loggingStore' import { join } from 'path' import * as fs from 'fs' import { app, net } from 'electron' import { handleAdbCommands } from './adbHandler' -import { Client, ClientManifest, ReplyData } from '@shared/types' +import { Client, ClientManifest, MESSAGE_TYPES, ReplyData, ReplyFn } from '@shared/types' import settingsStore from '../stores/settingsStore' +import { getLatestRelease } from './githubHandler' export const HandleDeviceData = async (data: string): Promise => { try { @@ -16,14 +17,11 @@ export const HandleDeviceData = async (data: string): Promise => { sendIpcData('version-status', deviceData) break default: - console.log('Unhandled response', deviceData) + loggingStore.log(MESSAGE_TYPES.ERROR, 'HandleDeviceData Unable to find device version') break } } catch (Exception) { - dataListener.asyncEmit( - MESSAGE_TYPES.ERROR, - 'HandleDeviceData encountered the error ' + Exception - ) + loggingStore.log(MESSAGE_TYPES.ERROR, 'HandleDeviceData encountered the error ' + Exception) } } @@ -33,7 +31,6 @@ export const getDeviceManifestVersion = async (deviceId: string): Promise void -): Promise => { +export const configureDevice = async (deviceId: string, reply?: ReplyFn): Promise => { const settings = await settingsStore.getSettings() - if (settings && settings.devicePort) { - console.error('Settings not found') - reply && reply('logging', { status: true, data: 'Opening Port...', final: false }) - try { - const response = await handleAdbCommands( - `-s ${deviceId} reverse tcp:${settings.devicePort} tcp:${settings.devicePort}` - ) - reply && reply('logging', { status: true, data: response || 'Port Opened', final: false }) - } catch (error) { - reply && reply('logging', { status: false, data: 'Unable to open port!', final: false }) + // Opens the socket port + try { + if (settings && settings.devicePort) { + console.error('Settings not found') + reply && reply('logging', { status: true, data: 'Opening Port...', final: false }) + try { + const response = await handleAdbCommands( + `-s ${deviceId} reverse tcp:${settings.devicePort} tcp:${settings.devicePort}`, + reply + ) + reply && reply('logging', { status: true, data: response || 'Port Opened', final: false }) + } catch (error) { + reply && reply('logging', { status: false, data: 'Unable to open port!', final: false }) + } + } else { + reply && + reply('logging', { + status: false, + data: 'Unable to open port!', + error: 'Device Port not found in settings', + final: false + }) + } + } catch (error) { + console.error('Error opening device port:', error) + if (error instanceof Error) { + reply && + reply('logging', { + status: false, + data: 'Unable to open port!', + error: error.message, + final: false + }) } - } else { - reply && - reply('logging', { - status: false, - data: 'Unable to open port!', - error: 'Device Port not found in settings', - final: false - }) } - const deviceManifestVersion = await getDeviceManifestVersion(deviceId) - const clientManifest = await getClientManifest(true, reply) - if (clientManifest && deviceManifestVersion !== clientManifest.version) { - try { - await HandlePushWebApp(deviceId, reply) - } catch (error) { + // Check if the client is already installed. Install it if it is missing + try { + const clientExists = await checkForClient(reply) + + if (!clientExists) { + // Download it from github + const repos = settings.clientRepos + reply && reply('logging', { status: true, data: 'Fetching Latest Client...', final: false }) + const latestReleases = await Promise.all( + repos.map(async (repo) => { + return await getLatestRelease(repo) + }) + ) + + // Sort releases by date and get the latest one + const clientAsset = latestReleases + .flatMap((release) => + release.assets.map((asset) => ({ ...asset, created_at: release.created_at })) + ) + .sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()) + .find((asset) => asset.name.includes('-client')) + + // Download it + if (clientAsset) { + reply && reply('logging', { status: true, data: 'Downloading Client...', final: false }) + await HandleWebappZipFromUrl(reply, clientAsset.browser_download_url) + + await new Promise((resolve) => { + setTimeout(async () => { + await checkForClient(reply) + resolve(null) + }, 5000) + }) + } else { + reply && + reply('logging', { + status: false, + data: 'No client asset found in latest releases', + final: false + }) + } + } + } catch (error) { + if (error instanceof Error) { reply && reply('logging', { status: false, - data: 'Unable to push webapp!', - error: typeof error == 'string' ? error : 'Unknown Error', + data: 'Unable to check for client!', + error: error.message, + final: false + }) + } else { + reply && + reply('logging', { + status: false, + data: 'Unable to check for client!', + error: 'Unknown error', final: false }) } - } else { + console.error('Error checking for client:', error) + } + + // Push the webapp to the device + try { reply && - reply('logging', { - status: true, - data: 'Device has the same webapp version!', - final: false - }) + reply('logging', { status: true, data: 'Fetching Device Manifest Version...', final: false }) + const deviceManifestVersion = await getDeviceManifestVersion(deviceId) + + reply && + reply('logging', { status: true, data: 'Fetching Client Manifest Version...', final: false }) + const clientManifest = await getClientManifest(reply) + if ( + clientManifest && + deviceManifestVersion && + deviceManifestVersion !== clientManifest.version + ) { + try { + reply && reply('logging', { status: true, data: 'Pushing client...', final: false }) + // Give a 30 second timeout to flash the webapp + await Promise.race([ + HandlePushWebApp(deviceId, reply), + new Promise((_, reject) => + setTimeout(() => reject('Timeout: Operation took longer than 30 seconds'), 30000) + ) + ]) + } catch (error) { + reply && + reply('logging', { + status: false, + data: 'Unable to push webapp!', + error: typeof error == 'string' ? error : 'Unknown Error', + final: false + }) + } + } else { + reply && + reply('logging', { + status: true, + data: 'Device has the same webapp version or doesnt exist!', + final: false + }) + } + } catch (error) { + if (error instanceof Error) { + reply && + reply('logging', { + status: false, + error: error.message, + data: 'Error pushing webapp!', + final: false + }) + } else { + reply && + reply('logging', { + status: false, + error: 'Unknown Error', + data: 'Error pushing webapp!', + final: false + }) + } + console.error('Error pushing webapp', error) } - reply && reply('logging', { status: true, data: 'Restarting Chromium', final: false }) try { - await handleAdbCommands(`-s ${deviceId} shell supervisorctl restart chromium`) + reply && reply('logging', { status: true, data: 'Restarting Chromium', final: false }) + await handleAdbCommands(`-s ${deviceId} shell supervisorctl restart chromium`, reply) } catch (error) { - reply && - reply('logging', { - status: false, - data: 'Unable to restart chromium!', - error: typeof error == 'string' ? error : 'Unknown Error', - final: false - }) + if (error instanceof Error) { + reply && + reply('logging', { + status: false, + data: 'Unable to restart chromium!', + error: error.message, + final: false + }) + } else { + reply && + reply('logging', { + status: false, + data: 'Unable to restart chromium!', + error: 'Unknown Error', + final: false + }) + } + console.error('Error restarting chromium', error) } - reply && reply('logging', { status: true, data: 'Device Configured!', final: true }) } export const HandlePushWebApp = async ( @@ -117,15 +237,32 @@ export const HandlePushWebApp = async ( try { const userDataPath = app.getPath('userData') const extractDir = join(userDataPath, 'webapp') + + const clientExists = await checkForClient(reply) + if (!clientExists) { + reply && + reply('logging', { + status: false, + data: 'Unable to push webapp!', + error: 'Client not found!', + final: false + }) + loggingStore.log( + MESSAGE_TYPES.ERROR, + '[HandlePushWebApp] Client Not Found! Ensure it is downloaded' + ) + return + } + let response - console.log('Remounting...') reply && reply('logging', { status: true, data: 'Remounting...', final: false }) response = await handleAdbCommands(`-s ${deviceId} shell mount -o remount,rw /`) - reply && reply('logging', { status: true, data: response || 'Moving...', final: false }) + reply && reply('logging', { status: true, data: response || 'Writing to tmp...', final: false }) response = await handleAdbCommands( `-s ${deviceId} shell mv /usr/share/qt-superbird-app/webapp /tmp/webapp-orig` ) - reply && reply('logging', { status: true, data: response || 'Moving...', final: false }) + reply && + reply('logging', { status: true, data: response || 'Moving from tmp...', final: false }) response = await handleAdbCommands( `-s ${deviceId} shell mv /tmp/webapp-orig /usr/share/qt-superbird-app/` ) @@ -147,7 +284,7 @@ export const HandlePushWebApp = async ( reply('logging', { status: false, data: 'There has been an error updating the client manifests ID.', - final: true, + final: false, error: `${error}` }) } @@ -175,29 +312,36 @@ export const HandlePushWebApp = async ( reply('logging', { status: false, data: 'There has been an error cleaning the client manifests ID.', - final: true, + final: false, error: `${error}` }) } - reply && reply('logging', { status: true, data: await response, final: true }) + reply && reply('logging', { status: true, data: await response, final: false }) } catch (Exception) { - reply && - reply('logging', { - status: false, - data: 'There has been an error', - final: true, - error: `${Exception}` - }) - dataListener.asyncEmit( - MESSAGE_TYPES.ERROR, - 'HandlePushWebApp encountered the error ' + Exception - ) + if (Exception instanceof Error) { + reply && + reply('logging', { + status: false, + data: 'Error while trying to push webapp!', + final: false, + error: Exception.message + }) + } else { + reply && + reply('logging', { + status: false, + data: 'Error while trying to push webapp!', + final: false, + error: `${Exception}` + }) + } + loggingStore.log(MESSAGE_TYPES.ERROR, 'HandlePushWebApp encountered the error ' + Exception) } } export const HandleWebappZipFromUrl = async ( - reply: (channel: string, data: ReplyData) => void, + reply: ReplyFn | undefined, zipFileUrl: string ): Promise => { const userDataPath = app.getPath('userData') @@ -210,7 +354,7 @@ export const HandleWebappZipFromUrl = async ( const AdmZip = await import('adm-zip') - reply('logging', { status: true, data: 'Downloading...', final: false }) + reply && reply('logging', { status: true, data: 'Downloading...', final: false }) const request = net.request(zipFileUrl) @@ -236,66 +380,68 @@ export const HandleWebappZipFromUrl = async ( // Optionally remove the temporary zip file fs.unlinkSync(tempZipPath) - - console.log(`Successfully extracted ${zipFileUrl} to ${extractDir}`) - dataListener.asyncEmit( + loggingStore.log( MESSAGE_TYPES.LOGGING, `Successfully extracted ${zipFileUrl} to ${extractDir}` ) // Notify success to the frontend - reply('logging', { status: true, data: 'Success!', final: true }) + reply && reply('logging', { status: true, data: 'Success!', final: false }) } catch (error) { console.error('Error extracting zip file:', error) - dataListener.asyncEmit(MESSAGE_TYPES.ERROR, `Error extracting zip file: ${error}`) + loggingStore.log(MESSAGE_TYPES.ERROR, `Error extracting zip file: ${error}`) // Notify failure to the frontend - reply('logging', { - status: false, - data: 'Failed to extract!', - final: true, - error: error instanceof Error ? error.message : String(error) - }) + reply && + reply('logging', { + status: false, + data: 'Failed to extract!', + final: false, + error: error instanceof Error ? error.message : String(error) + }) } }) response.on('error', (error) => { console.error('Error downloading zip file:', error) - dataListener.asyncEmit(MESSAGE_TYPES.ERROR, `Error downloading zip file: ${error}`) + loggingStore.log(MESSAGE_TYPES.ERROR, `Error downloading zip file: ${error}`) // Notify failure to the frontend - reply('logging', { - status: false, - data: 'ERR Downloading file!', - final: true, - error: error.message - }) + reply && + reply('logging', { + status: false, + data: 'ERR Downloading file!', + final: false, + error: error.message + }) }) } else { const errorMessage = `Failed to download zip file: ${response.statusCode}` console.error(errorMessage) - dataListener.asyncEmit(MESSAGE_TYPES.ERROR, errorMessage) + loggingStore.log(MESSAGE_TYPES.ERROR, errorMessage) // Notify failure to the frontend - reply('logging', { - status: false, - data: 'Failed to download zip file!', - final: true, - error: errorMessage - }) + reply && + reply('logging', { + status: false, + data: 'Failed to download zip file!', + final: false, + error: errorMessage + }) } }) request.on('error', (error) => { console.error('Error sending request:', error) - dataListener.asyncEmit(MESSAGE_TYPES.ERROR, `Error sending request: ${error}`) + loggingStore.log(MESSAGE_TYPES.ERROR, `Error sending request: ${error}`) // Notify failure to the frontend - reply('logging', { - status: false, - data: 'Failed to download zip file!', - final: true, - error: error.message - }) + reply && + reply('logging', { + status: false, + data: 'Failed to download zip file!', + final: false, + error: error.message + }) }) request.end() @@ -315,7 +461,7 @@ export const handleClientManifestUpdate = async ( await fs.promises.mkdir(extractDir, { recursive: true }) // Read the existing manifest - const existingManifest = await getClientManifest(true, reply) + const existingManifest = await getClientManifest(reply) // Merge the existing manifest with the partial updates const updatedManifest: Partial = { @@ -328,40 +474,56 @@ export const handleClientManifestUpdate = async ( // Write the updated manifest to the file await fs.promises.writeFile(manifestPath, manifestContent, 'utf8') - console.log('Manifest file updated successfully') - reply && reply('logging', { status: true, data: 'Manifest Updated!', final: true }) - dataListener.asyncEmit( - MESSAGE_TYPES.LOGGING, - 'DEVICE HANDLER: Manifest file updated successfully' - ) + loggingStore.log(MESSAGE_TYPES.LOGGING, 'Manifest file updated successfully') + reply && reply('logging', { status: true, data: 'Manifest Updated!', final: false }) + loggingStore.log(MESSAGE_TYPES.LOGGING, 'DEVICE HANDLER: Manifest file updated successfully') } catch (error) { console.error('Error updating manifest file:', error) - dataListener.asyncEmit( - MESSAGE_TYPES.ERROR, - 'DEVICE HANDLER: Error updating manifest file: ' + error - ) + loggingStore.log(MESSAGE_TYPES.ERROR, 'DEVICE HANDLER: Error updating manifest file: ' + error) + } +} + +export const checkForClient = async ( + reply?: (channel: string, data: ReplyData) => void +): Promise => { + reply && reply('logging', { status: true, data: 'Checking for client...', final: false }) + const userDataPath = app.getPath('userData') + const extractDir = join(userDataPath, 'webapp') + const manifestPath = join(extractDir, 'manifest.js') + + const manifestExists = fs.existsSync(manifestPath) + if (!manifestExists) { + loggingStore.log(MESSAGE_TYPES.LOGGING, 'Manifest file not found') + reply && + reply('logging', { + status: false, + data: 'Manifest file not found', + final: false + }) + loggingStore.log(MESSAGE_TYPES.ERROR, 'DEVICE HANDLER: Manifest file not found') } + return manifestExists } export const getClientManifest = async ( - utility: boolean = false, reply?: (channel: string, data: ReplyData) => void ): Promise => { - console.log('Getting manifest...') + loggingStore.log(MESSAGE_TYPES.LOGGING, 'Getting manifest...') const userDataPath = app.getPath('userData') const manifestPath = join(userDataPath, 'webapp', 'manifest.js') - console.log('manifestPath: ', manifestPath) + loggingStore.log(MESSAGE_TYPES.LOGGING, 'manifestPath: ' + manifestPath) if (!fs.existsSync(manifestPath)) { - console.log('Manifest file not found') + loggingStore.log(MESSAGE_TYPES.LOGGING, 'Manifest file not found') reply && reply('logging', { status: false, + error: 'Unable to find the client manifest!', data: 'Manifest file not found', - final: true + final: false }) - dataListener.asyncEmit( + loggingStore.log( MESSAGE_TYPES.ERROR, - 'DEVICE HANDLER: Client Manifest file not found! (Is the client downloaded?)' + 'DEVICE HANDLER: Client is not detected or downloaded! Please download the client! (downloads -> client)' ) return null } @@ -384,14 +546,13 @@ export const getClientManifest = async ( reply('logging', { status: true, data: 'Manifest loaded!', - final: !utility + final: false }) - dataListener.asyncEmit(MESSAGE_TYPES.LOGGING, 'DEVICE HANDLER: Manifest file read successfully') - console.log(manifest) + loggingStore.log(MESSAGE_TYPES.LOGGING, 'DEVICE HANDLER: Manifest file read successfully') return manifest } catch (error) { console.error('Error reading or parsing manifest file:', error) - dataListener.asyncEmit( + loggingStore.log( MESSAGE_TYPES.ERROR, 'DEVICE HANDLER: Error reading or parsing manifest file: ' + error ) @@ -399,7 +560,7 @@ export const getClientManifest = async ( reply('logging', { status: false, data: 'Unable to read Server Manifest file', - final: true, + final: false, error: 'Unable to read manifest file' + error }) return null @@ -442,7 +603,7 @@ export const SetupProxy = async ( final: false }) - console.log('Remounting...') + loggingStore.log(MESSAGE_TYPES.LOGGING, 'Remounting...') reply('logging', { status: true, data: 'Remounting...', final: false }) response = await handleAdbCommands(`-s ${deviceId} shell mount -o remount,rw /`) @@ -463,7 +624,7 @@ export const SetupProxy = async ( status: false, data: 'Error ensuring supervisor include: ' + error, error: 'Error ensuring supervisor include: ' + error, - final: true + final: false }) } @@ -502,14 +663,14 @@ user=root` reply('logging', { status: true, data: response || 'Supervisor updated configuration.', - final: true + final: false }) // Start the Supervisor program response = await handleAdbCommands(`-s ${deviceId} shell supervisorctl start setupProxy`) reply('logging', { status: true, data: response || 'Started setup-proxy program.', - final: true + final: false }) fs.unlinkSync(tempProxyConfPath) @@ -517,10 +678,11 @@ user=root` reply('logging', { status: false, data: 'There has been an error setting up the proxy.', - final: true, + final: false, error: `${Exception}` }) - dataListener.asyncEmit(MESSAGE_TYPES.ERROR, 'SetupProxy encountered the error ' + Exception) + loggingStore.log(MESSAGE_TYPES.ERROR, 'SetupProxy encountered the error ' + Exception) + throw new Error('SetupProxy encountered the error ' + Exception) } } @@ -624,19 +786,16 @@ export const AppendToSupervisor = async ( reply('logging', { status: true, data: 'Supervisor configuration updated and applied.', - final: true + final: false }) } catch (Exception) { reply('logging', { status: false, data: `Error appending to Supervisor: ${Exception}`, - final: true, + final: false, error: `${Exception}` }) - dataListener.asyncEmit( - MESSAGE_TYPES.ERROR, - 'AppendToSupervisor encountered the error ' + Exception - ) + loggingStore.log(MESSAGE_TYPES.ERROR, 'AppendToSupervisor encountered the error ' + Exception) } } @@ -712,23 +871,23 @@ files = /etc/supervisor.d/*.conf\n` reply('logging', { status: true, data: response || 'Supervisor updated configuration.', - final: true + final: false }) } else { reply('logging', { status: true, data: '[include] section already present. No need to update.', - final: true + final: false }) } } catch (Exception) { reply('logging', { status: false, data: `Error ensuring Supervisor [include] section: ${Exception}`, - final: true, + final: false, error: `${Exception}` }) - dataListener.asyncEmit( + loggingStore.log( MESSAGE_TYPES.ERROR, 'EnsureSupervisorInclude encountered the error ' + Exception ) diff --git a/DeskThingServer/src/main/handlers/firewallHandler.ts b/DeskThingServer/src/main/handlers/firewallHandler.ts index 7924a3e0..e97159ea 100644 --- a/DeskThingServer/src/main/handlers/firewallHandler.ts +++ b/DeskThingServer/src/main/handlers/firewallHandler.ts @@ -1,10 +1,10 @@ import { exec } from 'child_process' import os from 'os' -import dataListener, { MESSAGE_TYPES } from '../utils/events' +import loggingStore from '../stores/loggingStore' import fs from 'fs' import { join } from 'path' import { app } from 'electron' -import { ReplyFn } from '@shared/types' +import { ReplyFn, MESSAGE_TYPES } from '@shared/types' // Function to execute shell commands function runCommand(command: string): Promise { @@ -45,7 +45,7 @@ async function checkFirewallRuleExists(port: number): Promise { const result = await runCommand(checkCommand) return result.trim() === 'true' } else { - dataListener.asyncEmit(MESSAGE_TYPES.ERROR, `FIREWALL: Unsupported OS!`) + loggingStore.log(MESSAGE_TYPES.ERROR, `FIREWALL: Unsupported OS!`) console.error('Unsupported OS') return false } @@ -65,15 +65,18 @@ async function setupFirewall(port: number, reply?: ReplyFn): Promise { reply && reply('logging', { status: true, data: 'Checking if rules exist', final: false }) const ruleExists = await checkFirewallRuleExists(port) if (ruleExists) { - dataListener.asyncEmit( + loggingStore.log( MESSAGE_TYPES.LOGGING, `FIREWALL: Firewall rule for port ${port} verified successfully` ) - console.log(`Firewall rule for port ${port} verified successfully`) + loggingStore.log( + MESSAGE_TYPES.LOGGING, + `Firewall rule for port ${port} verified successfully` + ) reply && reply('logging', { status: true, data: 'Verified that the rule exists!', final: false }) } else { - dataListener.asyncEmit( + loggingStore.log( MESSAGE_TYPES.ERROR, `FIREWALL: Failed to verify firewall rule for port ${port}!` ) @@ -112,11 +115,11 @@ async function setupFirewall(port: number, reply?: ReplyFn): Promise { try { await runCommand(`powershell -ExecutionPolicy Bypass -File "${tempScriptPath}"`) - dataListener.asyncEmit( + loggingStore.log( MESSAGE_TYPES.LOGGING, `FIREWALL: Firewall rules set up successfully on Windows` ) - console.log('Firewall rules set up successfully on Windows') + loggingStore.log(MESSAGE_TYPES.LOGGING, 'Firewall rules set up successfully on Windows') reply && reply('logging', { status: true, data: 'Firewall ran without error', final: false }) @@ -140,11 +143,11 @@ async function setupFirewall(port: number, reply?: ReplyFn): Promise { ` await runCommand(`echo "${script}" | bash`) - dataListener.asyncEmit( + loggingStore.log( MESSAGE_TYPES.LOGGING, `FIREWALL: Firewall rules set up successfully on Linux` ) - console.log('Firewall rules set up successfully on Linux') + loggingStore.log(MESSAGE_TYPES.LOGGING, 'Firewall rules set up successfully on Linux') } else if (platform === 'darwin') { reply && reply('logging', { @@ -165,16 +168,16 @@ async function setupFirewall(port: number, reply?: ReplyFn): Promise { ` await runCommand(`echo "${script}" | bash`) - dataListener.asyncEmit( + loggingStore.log( MESSAGE_TYPES.LOGGING, `FIREWALL: Firewall rules set up successfully on macOS` ) - console.log('Firewall rules set up successfully on macOS') + loggingStore.log(MESSAGE_TYPES.LOGGING, 'Firewall rules set up successfully on macOS') } else { console.error('Unsupported OS') } } catch (error) { - dataListener.asyncEmit( + loggingStore.log( MESSAGE_TYPES.ERROR, `FIREWALL: Error encountered trying to setup firewall for ${port}! Run administrator and try again` ) diff --git a/DeskThingServer/src/main/handlers/githubHandler.ts b/DeskThingServer/src/main/handlers/githubHandler.ts index fb729e23..36792a0d 100644 --- a/DeskThingServer/src/main/handlers/githubHandler.ts +++ b/DeskThingServer/src/main/handlers/githubHandler.ts @@ -1,5 +1,31 @@ import { GithubRelease } from '@shared/types' +export async function getLatestRelease(repoUrl: string): Promise { + try { + // Extract the owner and repo from the URL + const repoMatch = repoUrl.match(/github\.com\/([^/]+)\/([^/]+)/) + if (!repoMatch) { + throw new Error('Invalid GitHub repository URL') + } + + const owner = repoMatch[1] + const repo = repoMatch[2] + + const apiUrl = `https://api.github.com/repos/${owner}/${repo}/releases/latest` + const response = await fetch(apiUrl) + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + + const release = await response.json() + return release + } catch (error) { + console.error('Error fetching latest release:', error) + throw error + } +} + export async function getReleases(repoUrl: string): Promise { try { // Extract the owner and repo from the URL diff --git a/DeskThingServer/src/main/handlers/musicHandler.ts b/DeskThingServer/src/main/handlers/musicHandler.ts index 0d9cc27f..2b4c6cd9 100644 --- a/DeskThingServer/src/main/handlers/musicHandler.ts +++ b/DeskThingServer/src/main/handlers/musicHandler.ts @@ -1,8 +1,9 @@ -import dataListener, { MESSAGE_TYPES } from '../utils/events' +import loggingStore from '../stores/loggingStore' import settingsStore from '../stores/settingsStore' -import { Settings, SocketData } from '@shared/types' +import { Settings, SocketData, MESSAGE_TYPES } from '@shared/types' import { sendMessageToApp } from '../services/apps' import { getAppByName } from './configHandler' +import appState from '../services/apps/appState' export class MusicHandler { private static instance: MusicHandler @@ -22,10 +23,13 @@ export class MusicHandler { private async initializeRefreshInterval(): Promise { const settings = await settingsStore.getSettings() // Get from your settings store + this.currentApp = settings.playbackLocation || 'none' + this.updateRefreshInterval(settings.refreshInterval) - dataListener.on(MESSAGE_TYPES.SETTINGS, this.handleSettingsUpdate) + settingsStore.addListener(this.handleSettingsUpdate.bind(this)) setTimeout(() => { + loggingStore.log(MESSAGE_TYPES.DEBUG, '[MusicHandler]: Initialized') this.refreshMusicData() }, 5000) // Delay to ensure settings are loaded } @@ -33,12 +37,12 @@ export class MusicHandler { private handleSettingsUpdate = (settings: Settings): void => { this.updateRefreshInterval(settings.refreshInterval) - dataListener.asyncEmit( + loggingStore.log( MESSAGE_TYPES.LOGGING, - `[MusicHandler]: Received settings update - checking for changes | Playback location: ${settings.playbackLocation}` + `[MusicHandler]: Received settings update - checking for changes | Playback location: ${this.currentApp} -> ${settings.playbackLocation}` ) if (settings.playbackLocation) { - dataListener.asyncEmit( + loggingStore.log( MESSAGE_TYPES.LOGGING, `[MusicHandler]: Setting restarting to use ${settings.playbackLocation}` ) @@ -53,7 +57,13 @@ export class MusicHandler { } if (refreshRate < 0) { - dataListener.asyncEmit(MESSAGE_TYPES.LOGGING, `[MusicHandler]: Cancelling Refresh Interval!`) + loggingStore.log(MESSAGE_TYPES.LOGGING, `[MusicHandler]: Cancelling Refresh Interval!`) + return + } else if (refreshRate < 5) { + loggingStore.log( + MESSAGE_TYPES.WARNING, + `[MusicHandler]: Refresh Interval is ${refreshRate}! Performance may be impacted` + ) return } @@ -62,66 +72,134 @@ export class MusicHandler { }, refreshRate) } - private async refreshMusicData(): Promise { - if (!this.currentApp || this.currentApp.length == 0) { - dataListener.asyncEmit( - MESSAGE_TYPES.ERROR, - `[MusicHandler]: No playback location set! Go to settings -> Music to set the playback location!` + private async findCurrentPlaybackSource(): Promise { + const Apps = appState.getAllBase() + + const audioSource = Apps.find((app) => app.manifest?.isAudioSource) + + if (audioSource) { + loggingStore.log( + MESSAGE_TYPES.WARNING, + `[MusicHandler]: Found ${audioSource.name} as an audio source automatically. Applying.` ) - return + return audioSource.name + } else { + loggingStore.log( + MESSAGE_TYPES.LOGGING, + `[MusicHandler]: Unable to automatically set an audio source. No app found!` + ) + return null + } + } + + private async getPlaybackSource(): Promise { + if (this.currentApp == 'disabled') { + loggingStore.log( + MESSAGE_TYPES.LOGGING, + `[MusicHandler]: Music is disabled! Cancelling refresh` + ) + const settings = await settingsStore.getSettings() + if (settings.refreshInterval > 0) { + settingsStore.updateSetting('refreshInterval', -1) + } + return null + } + + if (this.currentApp == 'none') { + const app = await this.findCurrentPlaybackSource() + if (app) { + this.currentApp = app + settingsStore.updateSetting('playbackLocation', app) + return app + } else { + loggingStore.log( + MESSAGE_TYPES.ERROR, + `[MusicHandler]: No Audiosource Found! Go to Downloads -> Apps and download an audio source! (Spotify, MediaWin, GMP, etc)` + ) + return null + } + } + + if (!this.currentApp || this.currentApp.length == 0) { + // Attempt to get audiosource from settings + const currentApp = (await settingsStore.getSettings()).playbackLocation + if (!currentApp || currentApp.length == 0) { + loggingStore.log( + MESSAGE_TYPES.ERROR, + `[MusicHandler]: No playback location set! Go to settings -> Music to set the playback location!` + ) + return null + } else { + loggingStore.log( + MESSAGE_TYPES.WARNING, + `[MusicHandler]: Playback location was not set! Setting to ${currentApp}` + ) + this.currentApp = currentApp + } } const app = await getAppByName(this.currentApp) if (!app || app.running == false) { - dataListener.asyncEmit( + loggingStore.log( MESSAGE_TYPES.ERROR, - `[MusicHandler]: App ${this.currentApp} not found or not running!!` + `[MusicHandler]: App ${this.currentApp} is not found or not running!` ) + return null + } + + return this.currentApp + } + + private async refreshMusicData(): Promise { + const currentApp = await this.getPlaybackSource() + + if (!currentApp) { + return } try { - await sendMessageToApp(this.currentApp, { type: 'get', request: 'refresh', payload: '' }) - dataListener.asyncEmit(MESSAGE_TYPES.LOGGING, `[MusicHandler]: Refreshing Music Data!`) + await sendMessageToApp(currentApp, { type: 'get', request: 'refresh', payload: '' }) + loggingStore.log(MESSAGE_TYPES.LOGGING, `[MusicHandler]: Refreshing Music Data!`) } catch (error) { - dataListener.asyncEmit(MESSAGE_TYPES.ERROR, `[MusicHandler]: Music refresh failed: ${error}`) + loggingStore.log(MESSAGE_TYPES.ERROR, `[MusicHandler]: Music refresh failed: ${error}`) } } - public async handleClientRequest(request: SocketData): Promise { - if (!this.currentApp) { - const settings = await settingsStore.getSettings() - if (settings.playbackLocation) { - this.currentApp = settings.playbackLocation - } else { - dataListener.asyncEmit(MESSAGE_TYPES.ERROR, `[MusicHandler]: No playback location set!`) - return - } - } - - if (this.currentApp == 'none') { - dataListener.asyncEmit( + public async setAudioSource(source: string): Promise { + if (source.length == 0) { + loggingStore.log( MESSAGE_TYPES.ERROR, - `[MusicHandler]: Playback location is 'none' ! Go to settings -> Music to set the playback location!` + `[MusicHandler]: Unable to update playback location. No playback location passed!` ) return } + loggingStore.log( + MESSAGE_TYPES.LOGGING, + `[MusicHandler]: Setting Playback Location to ${source}` + ) + this.currentApp = source + } + + public async handleClientRequest(request: SocketData): Promise { + const currentApp = await this.getPlaybackSource() + + if (!currentApp) { + return + } if (request.app != 'music' && request.app != 'utility') return if (request.app == 'utility') { - dataListener.asyncEmit( + loggingStore.log( MESSAGE_TYPES.LOGGING, `[MusicHandler]: Legacy Name called! Support for this will be dropped in future updates. Migrate your app to use 'music' instead!` ) } - dataListener.asyncEmit( - MESSAGE_TYPES.LOGGING, - `[MusicHandler]: ${request.type} ${request.request}` - ) + loggingStore.log(MESSAGE_TYPES.LOGGING, `[MusicHandler]: ${request.type} ${request.request}`) - sendMessageToApp(this.currentApp, { + sendMessageToApp(currentApp, { type: request.type, request: request.request, payload: request.payload diff --git a/DeskThingServer/src/main/handlers/utilityHandler.ts b/DeskThingServer/src/main/handlers/utilityHandler.ts index 53b64fac..2efb75a2 100644 --- a/DeskThingServer/src/main/handlers/utilityHandler.ts +++ b/DeskThingServer/src/main/handlers/utilityHandler.ts @@ -1,13 +1,20 @@ -import { ButtonMapping, Client, GithubRelease, Settings } from '@shared/types' -import { ReplyFn, UtilityIPCData } from '@shared/types/ipcTypes' +import { + ReplyFn, + UtilityIPCData, + ButtonMapping, + Client, + MESSAGE_TYPES, + GithubRelease, + Log, + Settings +} from '@shared/types' import ConnectionStore from '../stores/connectionsStore' import settingsStore from '../stores/settingsStore' import { getReleases } from './githubHandler' -import dataListener, { MESSAGE_TYPES } from '../utils/events' +import loggingStore from '../stores/loggingStore' import path from 'path' import { shell, app, dialog } from 'electron' import keyMapStore from '../stores/keyMapStore' -import logger from '../utils/logger' import { setupFirewall } from './firewallHandler' import { disconnectClient } from '../services/client/clientCom' import { restartServer } from '../services/client/websocket' @@ -18,11 +25,19 @@ export const utilityHandler: Record< data: UtilityIPCData, replyFn: ReplyFn ) => Promise< - void | string | Client[] | boolean | string[] | Settings | GithubRelease[] | ButtonMapping + | void + | string + | Client[] + | boolean + | string[] + | Settings + | GithubRelease[] + | ButtonMapping + | Log[] > > = { ping: async () => { - console.log('Pinged! pong') + loggingStore.log(MESSAGE_TYPES.LOGGING, 'Pinged! pong') return 'pong' }, zip: async (data): Promise => { @@ -63,14 +78,18 @@ export const utilityHandler: Record< try { return await getReleases(data.payload) } catch (error) { - dataListener.asyncEmit(MESSAGE_TYPES.ERROR, error) + if (error instanceof Error) { + loggingStore.log(MESSAGE_TYPES.ERROR, error.message) + } else { + loggingStore.log(MESSAGE_TYPES.ERROR, String(error)) + } return [] } }, logs: async (data) => { switch (data.request) { case 'get': - return await logger.getLogs() + return await loggingStore.getLogs() default: return } @@ -132,14 +151,13 @@ const refreshFirewall = async (replyFn: ReplyFn): Promise => { replyFn('logging', { status: true, data: 'Refreshing Firewall', final: false }) const payload = (await settingsStore.getSettings()) as Settings if (payload) { - dataListener.asyncEmit(MESSAGE_TYPES.LOGGING, '[firewall] Setting up firewall') + loggingStore.log(MESSAGE_TYPES.LOGGING, '[firewall] Setting up firewall') try { await setupFirewall(payload.devicePort, replyFn) } catch (firewallError) { - console.log(firewallError) if (!(firewallError instanceof Error)) return - dataListener.asyncEmit(MESSAGE_TYPES.ERROR, `FIREWALL: ${firewallError.message}`) + loggingStore.log(MESSAGE_TYPES.ERROR, `FIREWALL: ${firewallError.message}`) replyFn('logging', { status: false, data: 'Error in firewall', @@ -149,7 +167,7 @@ const refreshFirewall = async (replyFn: ReplyFn): Promise => { return } } else { - dataListener.asyncEmit(MESSAGE_TYPES.ERROR, '[firewall] No settings found!') + loggingStore.log(MESSAGE_TYPES.ERROR, '[firewall] No settings found!') replyFn('logging', { status: false, data: 'Error in firewall', @@ -158,7 +176,7 @@ const refreshFirewall = async (replyFn: ReplyFn): Promise => { }) } } catch (error) { - dataListener.asyncEmit(MESSAGE_TYPES.ERROR, 'SERVER: [firewall] Error saving manifest' + error) + loggingStore.log(MESSAGE_TYPES.ERROR, 'SERVER: [firewall] Error saving manifest' + error) console.error('[Firewall] Error setting client manifest:', error) if (error instanceof Error) { replyFn('logging', { status: false, data: 'Unfinished', error: error.message, final: true }) diff --git a/DeskThingServer/src/main/index.ts b/DeskThingServer/src/main/index.ts index d5c65046..08dfa4c6 100644 --- a/DeskThingServer/src/main/index.ts +++ b/DeskThingServer/src/main/index.ts @@ -1,4 +1,4 @@ -import { AppIPCData, AuthScopes, Client, UtilityIPCData } from '@shared/types' +import { AppIPCData, AuthScopes, Client, UtilityIPCData, MESSAGE_TYPES } from '@shared/types' import { app, shell, BrowserWindow, ipcMain, Tray, Menu, nativeImage } from 'electron' import { join, resolve } from 'path' import icon from '../../resources/icon.png?asset' @@ -213,17 +213,16 @@ async function initializeDoc(): Promise { } async function setupIpcHandlers(): Promise { - const dataListener = (await import('./utils/events')).default - const { MESSAGE_TYPES } = await import('./utils/events') + const loggingStore = (await import('./stores/loggingStore')).default const { appHandler } = await import('./handlers/appHandler') const { clientHandler } = await import('./handlers/clientHandler') const { utilityHandler } = await import('./handlers/utilityHandler') - const { ResponseLogger } = await import('./utils/events') + const { ResponseLogger } = await import('./stores/loggingStore') const defaultHandler = async (data: AppIPCData): Promise => { console.error(`No handler implemented for type: ${data.type} ${data}`) - dataListener.asyncEmit(MESSAGE_TYPES.ERROR, `No handler implemented for type: ${data.type}`) + loggingStore.log(MESSAGE_TYPES.ERROR, `No handler implemented for type: ${data.type}`) } ipcMain.handle('APPS', async (event, data: AppIPCData) => { @@ -238,7 +237,7 @@ async function setupIpcHandlers(): Promise { } } catch (error) { console.error('Error in IPC handler:', error) - dataListener.asyncEmit(MESSAGE_TYPES.ERROR, `Error in IPC handler: ${error}`) + loggingStore.log(MESSAGE_TYPES.ERROR, `Error in IPC handler: ${error}`) } }) @@ -254,12 +253,11 @@ async function setupIpcHandlers(): Promise { } } catch (error) { console.error('Error in IPC handler:', error) - dataListener.asyncEmit(MESSAGE_TYPES.ERROR, `Error in IPC handler: ${error}`) + loggingStore.log(MESSAGE_TYPES.ERROR, `Error in IPC handler: ${error}`) } }) ipcMain.handle('UTILITY', async (event, data: UtilityIPCData) => { - console.log('Received IPC data:', data) const handler = utilityHandler[data.type] || defaultHandler const replyFn = ResponseLogger(event.sender.send.bind(event.sender)) @@ -272,19 +270,13 @@ async function setupIpcHandlers(): Promise { } } catch (error) { console.error('Error in IPC handler:', error) - dataListener.asyncEmit(MESSAGE_TYPES.ERROR, `Error in IPC handler: ${error}`) + loggingStore.log(MESSAGE_TYPES.ERROR, `Error in IPC handler: ${error}`) } }) - dataListener.on(MESSAGE_TYPES.ERROR, (errorData) => { - sendIpcData('error', errorData) - }) - dataListener.on(MESSAGE_TYPES.LOGGING, (errorData) => { + loggingStore.addListener((errorData) => { sendIpcData('log', errorData) }) - dataListener.on(MESSAGE_TYPES.MESSAGE, (errorData) => { - sendIpcData('message', errorData) - }) ConnectionStore.on((clients: Client[]) => { sendIpcData('connections', { status: true, data: clients.length, final: true }) sendIpcData('clients', { status: true, data: clients, final: true }) @@ -292,7 +284,7 @@ async function setupIpcHandlers(): Promise { ConnectionStore.onDevice((devices: string[]) => { sendIpcData('adbdevices', { status: true, data: devices, final: true }) }) - dataListener.on(MESSAGE_TYPES.SETTINGS, (newSettings) => { + settingsStore.addListener((newSettings) => { sendIpcData('settings-updated', newSettings) }) } @@ -367,8 +359,6 @@ function handleUrl(url: string | undefined): void { if (url && url.startsWith('deskthing://')) { const path = url.replace('deskthing://', '') - console.log('Handling URL:', url, path) - if (mainWindow) { mainWindow.webContents.send('handle-protocol-url', path) } diff --git a/DeskThingServer/src/main/services/apps/appCommunication.ts b/DeskThingServer/src/main/services/apps/appCommunication.ts index c643b348..fea47498 100644 --- a/DeskThingServer/src/main/services/apps/appCommunication.ts +++ b/DeskThingServer/src/main/services/apps/appCommunication.ts @@ -1,6 +1,6 @@ import { openAuthWindow, sendIpcAuthMessage } from '../..' -import { AuthScopes, IncomingData, Key, Action, ToClientType } from '@shared/types' -import dataListener, { MESSAGE_TYPES } from '../../utils/events' +import { AuthScopes, MESSAGE_TYPES, IncomingData, Key, Action, ToClientType } from '@shared/types' +import loggingStore from '../../stores/loggingStore' import { ipcMain } from 'electron' /** @@ -18,7 +18,7 @@ export async function handleDataFromApp(app: string, appData: IncomingData): Pro switch (appData.type) { case 'message': - dataListener.asyncEmit(MESSAGE_TYPES.MESSAGE, appData.payload) + loggingStore.log(MESSAGE_TYPES.MESSAGE, appData.payload, app.toUpperCase()) break case 'get': switch (appData.request) { @@ -67,7 +67,7 @@ export async function handleDataFromApp(app: string, appData: IncomingData): Pro if (appData.payload && appData.request) { sendMessageToApp(appData.request, appData.payload) } else { - dataListener.asyncEmit( + loggingStore.log( MESSAGE_TYPES.ERROR, `${app.toUpperCase()}: App data malformed`, appData.payload @@ -75,10 +75,10 @@ export async function handleDataFromApp(app: string, appData: IncomingData): Pro } break case 'error': - dataListener.asyncEmit(MESSAGE_TYPES.ERROR, `${app.toUpperCase()}: ${appData.payload}`) + loggingStore.log(MESSAGE_TYPES.ERROR, `${appData.payload}`, app.toUpperCase()) break case 'log': - dataListener.asyncEmit(MESSAGE_TYPES.LOGGING, `${app.toUpperCase()}: ${appData.payload}`) + loggingStore.log(MESSAGE_TYPES.LOGGING, `${appData.payload}`, app.toUpperCase()) break case 'button': if (appData.request == 'add') { @@ -90,16 +90,16 @@ export async function handleDataFromApp(app: string, appData: IncomingData): Pro source: app, version: appData.payload.version || '0.0.0', enabled: true, - flavors: appData.payload.flavors || [] + Modes: appData.payload.Modes || [] } keyMapStore.addKey(Key) - dataListener.asyncEmit( + loggingStore.log( MESSAGE_TYPES.LOGGING, `${app.toUpperCase()}: Added Button Successfully` ) } } catch (Error) { - dataListener.asyncEmit(MESSAGE_TYPES.ERROR, `${app.toUpperCase()}: ${Error}`) + loggingStore.log(MESSAGE_TYPES.ERROR, `${app.toUpperCase()}: ${Error}`) } } else if (appData.request == 'remove') { keyMapStore.removeKey(appData.payload.id) @@ -122,13 +122,13 @@ export async function handleDataFromApp(app: string, appData: IncomingData): Pro source: app } keyMapStore.addAction(Action) - dataListener.asyncEmit( + loggingStore.log( MESSAGE_TYPES.LOGGING, `${app.toUpperCase()}: Added Action Successfully` ) } } catch (Error) { - dataListener.asyncEmit(MESSAGE_TYPES.ERROR, `${app.toUpperCase()}: ${Error}`) + loggingStore.log(MESSAGE_TYPES.ERROR, `${app.toUpperCase()}: ${Error}`) } break case 'remove': @@ -172,7 +172,7 @@ export async function requestUserInput(appName: string, scope: AuthScopes): Prom export async function sendMessageToApp(appName: string, data: IncomingData): Promise { const { AppHandler } = await import('./appState') const appHandler = AppHandler.getInstance() - dataListener.asyncEmit( + loggingStore.log( MESSAGE_TYPES.LOGGING, `[sendMessageToApp] Sending message to ${appName} with ${data.type}` ) @@ -181,7 +181,7 @@ export async function sendMessageToApp(appName: string, data: IncomingData): Pro if (app && typeof app.func.toClient === 'function') { ;(app.func.toClient as ToClientType)(data) } else { - dataListener.asyncEmit( + loggingStore.log( MESSAGE_TYPES.ERROR, `SERVER: App ${appName} not found or does not have toClient function. (is it running?)` ) diff --git a/DeskThingServer/src/main/services/apps/appInstaller.ts b/DeskThingServer/src/main/services/apps/appInstaller.ts index 8f8ab25c..c10fe039 100644 --- a/DeskThingServer/src/main/services/apps/appInstaller.ts +++ b/DeskThingServer/src/main/services/apps/appInstaller.ts @@ -1,13 +1,14 @@ import { join, resolve } from 'path' import { app } from 'electron' -import dataListener, { MESSAGE_TYPES } from '../../utils/events' +import loggingStore from '../../stores/loggingStore' import { IncomingData, ToClientType, Response, Manifest, DeskThing, - AppReturnData + AppReturnData, + MESSAGE_TYPES } from '@shared/types' import { getAppFilePath, getManifest } from './appUtils' import { mkdirSync, existsSync, rmSync, promises } from 'node:fs' @@ -29,11 +30,14 @@ import { handleDataFromApp } from './appCommunication' export async function handleZip(zipFilePath: string, reply?): Promise { const { getManifest } = await import('./appUtils') try { - console.log(`[handleZip] Extracting ${zipFilePath}...`) + loggingStore.log(MESSAGE_TYPES.LOGGING, `[handleZip] Extracting ${zipFilePath}...`) const appPath = join(app.getPath('userData'), 'apps') // Extract to user data folder // Create the extraction directory if it doesn't exist if (!existsSync(appPath)) { - console.log(`[handleZip] Creating extraction directory at ${appPath}...`) + loggingStore.log( + MESSAGE_TYPES.LOGGING, + `[handleZip] Creating extraction directory at ${appPath}...` + ) mkdirSync(appPath, { recursive: true }) } @@ -42,7 +46,10 @@ export async function handleZip(zipFilePath: string, reply?): Promise((resolve, reject) => { try { - console.log(`[handleZip] Extracting app...`) + loggingStore.log(MESSAGE_TYPES.LOGGING, `[handleZip] Extracting app...`) const zip = new AdmZip.default(zipFilePath) zip.getEntries().forEach((entry) => { if (entry.isDirectory) { - console.log(`[handleZip] Skipping directory ${entry.entryName}`) + loggingStore.log( + MESSAGE_TYPES.LOGGING, + `[handleZip] Skipping directory ${entry.entryName}` + ) } else { - console.log(`[handleZip] Extracting file ${entry.entryName}`) + loggingStore.log( + MESSAGE_TYPES.LOGGING, + `[handleZip] Extracting file ${entry.entryName}` + ) zip.extractEntryTo(entry, tempDir, true, true) } }) zip.extractAllTo(tempDir, true) - console.log(`[handleZip] App extracted to ${tempDir}`) + loggingStore.log(MESSAGE_TYPES.LOGGING, `[handleZip] App extracted to ${tempDir}`) resolve() } catch (error) { - dataListener.asyncEmit(MESSAGE_TYPES.ERROR, `SERVER: Error extracting ${zipFilePath}`) + loggingStore.log(MESSAGE_TYPES.ERROR, `SERVER: Error extracting ${zipFilePath}`) reply && reply('logging', { status: false, data: 'Extraction failed!', final: true }) reject(error) } @@ -80,12 +93,9 @@ export async function handleZip(zipFilePath: string, reply?): Promise { if (app && typeof app.func.start === 'function') { app.func.start() } else { - console.log(`App ${appName} not found.`) + loggingStore.log(MESSAGE_TYPES.LOGGING, `App ${appName} not found.`) } const DeskThing = await getDeskThing(appName) @@ -261,7 +271,6 @@ export async function run(appName: string): Promise { const manifestResponse: Response = await DeskThing.getManifest() - // Check if all required apps are running (I know this can be better...) const manifest = await handleManifest(appName, manifestResponse) if (!manifest) { @@ -271,17 +280,17 @@ export async function run(appName: string): Promise { appState.appendManifest(manifest, appName) } - dataListener.asyncEmit(MESSAGE_TYPES.LOGGING, `Configuring ${appName}!`) + loggingStore.log(MESSAGE_TYPES.LOGGING, `Configuring ${appName}!`) await setupFunctions(appName, DeskThing) - dataListener.asyncEmit(MESSAGE_TYPES.LOGGING, `Running ${appName}!`) + loggingStore.log(MESSAGE_TYPES.LOGGING, `Running ${appName}!`) const result = await start(appName) if (!result) { - dataListener.asyncEmit(MESSAGE_TYPES.ERROR, `App ${appName} failed to start!`) + loggingStore.log(MESSAGE_TYPES.ERROR, `App ${appName} failed to start!`) } } catch (error) { - dataListener.asyncEmit(MESSAGE_TYPES.ERROR, `Error running app ${error}`) + loggingStore.log(MESSAGE_TYPES.ERROR, `Error running app ${error}`) console.error('Error running app:', error) } } @@ -297,17 +306,14 @@ export const start = async (appName: string): Promise => { const appInstance = appState.get(appName) if (!appInstance || !appInstance.func.start || appInstance.func.start === undefined) { - dataListener.asyncEmit( - MESSAGE_TYPES.ERROR, - `App ${appName} not found. or not started correctly` - ) + loggingStore.log(MESSAGE_TYPES.ERROR, `App ${appName} not found. or not started correctly`) return false } // Check if all required apps are running const manifest = appInstance.manifest if (!manifest) { - dataListener.asyncEmit(MESSAGE_TYPES.ERROR, `App ${appName} not found.`) + loggingStore.log(MESSAGE_TYPES.ERROR, `App ${appName} not found.`) return false } @@ -315,7 +321,7 @@ export const start = async (appName: string): Promise => { const requiredApps = manifest.requires || [] for (const requiredApp of requiredApps) { if (!appState.getOrder().includes(requiredApp) && requiredApp.length > 2) { - dataListener.asyncEmit( + loggingStore.log( MESSAGE_TYPES.ERROR, `Unable to run ${appName}! This app requires '${requiredApp}' to be enabled and running.` ) @@ -328,20 +334,20 @@ export const start = async (appName: string): Promise => { try { const startResponse: Response = await appInstance.func.start() if (startResponse.status == 200) { - dataListener.asyncEmit( + loggingStore.log( MESSAGE_TYPES.MESSAGE, `App ${appName} started successfully with response ${startResponse.data.message}` ) return true } else { - dataListener.asyncEmit( + loggingStore.log( MESSAGE_TYPES.ERROR, `App ${appName} failed to start with response ${startResponse.data.message}` ) return false } } catch (error) { - dataListener.asyncEmit(MESSAGE_TYPES.ERROR, `Error starting app ${error}`) + loggingStore.log(MESSAGE_TYPES.ERROR, `Error starting app ${error}`) console.error('Error starting app:', error) } return false @@ -361,7 +367,7 @@ const setupFunctions = async (appName: string, DeskThing: DeskThing): Promise => { return DeskThing.start({ toServer: (data) => handleDataFromApp(appName, data), - SysEvents: (event: string, listener: (...args: string[]) => void) => { - dataListener.on(event, listener) // Add event listener - return () => dataListener.removeListener(event, listener) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + SysEvents: (_event: string, _listener: (...args: string[]) => void) => { + return () => { + /* do something with this to let apps listen for server events like apps being added or settings being changed */ + } } }) } @@ -387,7 +395,7 @@ const setupFunctions = async (appName: string, DeskThing: DeskThing): Promise => { } else if (existsSync(appEntryPointCjs)) { appEntryPoint = appEntryPointCjs } else { - dataListener.asyncEmit( + loggingStore.log( MESSAGE_TYPES.ERROR, `Entry point for app ${appName} not found. (Does it have an index.js file?)` ) diff --git a/DeskThingServer/src/main/services/apps/appManager.ts b/DeskThingServer/src/main/services/apps/appManager.ts index 5df36a6a..8e1a3ca2 100644 --- a/DeskThingServer/src/main/services/apps/appManager.ts +++ b/DeskThingServer/src/main/services/apps/appManager.ts @@ -1,6 +1,6 @@ import { rmSync, readdirSync, statSync, existsSync } from 'node:fs' -import dataListener, { MESSAGE_TYPES } from '../../utils/events' - +import loggingStore from '../../stores/loggingStore' +import { MESSAGE_TYPES } from '@shared/types' export async function clearCache(appName: string): Promise { try { const { join } = await import('path') @@ -21,24 +21,41 @@ export async function clearCache(appName: string): Promise { const resolvedPath = require.resolve(itemPath) if (require.cache[resolvedPath]) { delete require.cache[resolvedPath] - dataListener.asyncEmit( - MESSAGE_TYPES.LOGGING, - `SERVER: Removed ${resolvedPath} from cache` + loggingStore.log(MESSAGE_TYPES.LOGGING, `SERVER: Removed ${resolvedPath} from cache`) + } else { + loggingStore.log(MESSAGE_TYPES.LOGGING, `SERVER: ${resolvedPath} not in cache!`) + } + } catch (error) { + if (error instanceof Error) { + loggingStore.log( + MESSAGE_TYPES.ERROR, + `SERVER: Error clearing cache for ${itemPath}:`, + error.message ) } else { - dataListener.asyncEmit(MESSAGE_TYPES.LOGGING, `SERVER: ${resolvedPath} not in cache!`) + loggingStore.log( + MESSAGE_TYPES.ERROR, + `SERVER: Error clearing cache for ${itemPath}:`, + String(error) + ) } - } catch (e) { - dataListener.asyncEmit(MESSAGE_TYPES.LOGGING, `SERVER: clearCache Error`, e) } } }) } catch (error) { - dataListener.asyncEmit( - MESSAGE_TYPES.LOGGING, - `SERVER: Error clearing cache for directory ${appName}:`, - error - ) + if (error instanceof Error) { + loggingStore.log( + MESSAGE_TYPES.ERROR, + `SERVER: Error clearing cache for directory ${appName}:`, + error.message + ) + } else { + loggingStore.log( + MESSAGE_TYPES.ERROR, + `SERVER: Error clearing cache for directory ${appName}:`, + String(error) + ) + } } } @@ -49,7 +66,7 @@ export async function clearCache(appName: string): Promise { */ export async function purgeApp(appName: string): Promise { try { - dataListener.asyncEmit(MESSAGE_TYPES.LOGGING, `SERVER: Purging App ${appName}`) + loggingStore.log(MESSAGE_TYPES.LOGGING, `SERVER: Purging App ${appName}`) const { purgeAppData } = await import('../../handlers/dataHandler') const { purgeAppConfig } = await import('../../handlers/configHandler') @@ -74,9 +91,9 @@ export async function purgeApp(appName: string): Promise { // Remove the file from filesystem if (existsSync(dir)) { await rmSync(dir, { recursive: true, force: true }) - console.log(`Purged all data for app ${appName}`) + loggingStore.log(MESSAGE_TYPES.LOGGING, `Purged all data for app ${appName}`) } - dataListener.asyncEmit(MESSAGE_TYPES.LOGGING, `SERVER: Purged App ${appName}`) + loggingStore.log(MESSAGE_TYPES.LOGGING, `SERVER: Purged App ${appName}`) } catch (error) { console.error(`Error purging app data for ${appName}`, error) } diff --git a/DeskThingServer/src/main/services/apps/appRunner.ts b/DeskThingServer/src/main/services/apps/appRunner.ts index 9d279db2..627ce125 100644 --- a/DeskThingServer/src/main/services/apps/appRunner.ts +++ b/DeskThingServer/src/main/services/apps/appRunner.ts @@ -1,4 +1,5 @@ -import dataListener, { MESSAGE_TYPES } from '../../utils/events' +import loggingStore from '../../stores/loggingStore' +import { MESSAGE_TYPES } from '@shared/types' /** * Loads and runs all enabled apps from appData.json @@ -12,12 +13,12 @@ export async function loadAndRunEnabledApps(): Promise { try { const appInstances = appHandler.getAll() - dataListener.asyncEmit(MESSAGE_TYPES.LOGGING, 'SERVER: Loaded apps config. Running apps...') + loggingStore.log(MESSAGE_TYPES.LOGGING, 'SERVER: Loaded apps config. Running apps...') const enabledApps = appInstances.filter((appConfig) => appConfig.enabled === true) await Promise.all( enabledApps.map(async (appConfig) => { - dataListener.asyncEmit( + loggingStore.log( MESSAGE_TYPES.LOGGING, `SERVER: Automatically running app ${appConfig.name}` ) @@ -29,15 +30,12 @@ export async function loadAndRunEnabledApps(): Promise { await Promise.all( failedApps.map(async (failedApp) => { - dataListener.asyncEmit( - MESSAGE_TYPES.LOGGING, - `SERVER: Attempting to run ${failedApp.name} again` - ) + loggingStore.log(MESSAGE_TYPES.LOGGING, `SERVER: Attempting to run ${failedApp.name} again`) await appHandler.run(failedApp.name) }) ) } catch (error) { - dataListener.asyncEmit( + loggingStore.log( MESSAGE_TYPES.ERROR, `SERVER: Error loading and running enabled apps: ${error}` ) diff --git a/DeskThingServer/src/main/services/apps/appState.ts b/DeskThingServer/src/main/services/apps/appState.ts index e494ded1..2eeae632 100644 --- a/DeskThingServer/src/main/services/apps/appState.ts +++ b/DeskThingServer/src/main/services/apps/appState.ts @@ -1,5 +1,6 @@ -import { App, AppInstance, Manifest, AppReturnData } from '@shared/types' +import { App, AppInstance, Manifest, AppReturnData, MESSAGE_TYPES } from '@shared/types' import { sendConfigData, sendSettingsData } from '../client/clientCom' +import loggingStore from '../../stores/loggingStore' /** * TODO: Sync with the file @@ -32,9 +33,11 @@ export class AppHandler { * Loads the apps from file */ async loadApps(): Promise { - console.log('[appState] [loadApps]: Loading apps...') + loggingStore.log(MESSAGE_TYPES.LOGGING, '[appState] [loadApps]: Loading apps...') const { getAppData } = await import('../../handlers/configHandler') + const data = await getAppData() + data.apps.forEach((app) => { if (this.apps[app.name]) { // Update existing app instance with stored data diff --git a/DeskThingServer/src/main/services/apps/appUtils.ts b/DeskThingServer/src/main/services/apps/appUtils.ts index 3999ffae..272249d5 100644 --- a/DeskThingServer/src/main/services/apps/appUtils.ts +++ b/DeskThingServer/src/main/services/apps/appUtils.ts @@ -1,7 +1,7 @@ -import { Manifest } from '@shared/types' +import { Manifest, MESSAGE_TYPES } from '@shared/types' import { join } from 'path' import { existsSync, promises } from 'node:fs' -import dataListener, { MESSAGE_TYPES } from '../../utils/events' +import loggingStore from '../../stores/loggingStore' import { app } from 'electron' let devAppPath: string @@ -16,7 +16,7 @@ let devAppPath: string */ export async function getManifest(fileLocation: string): Promise { try { - console.log('[getManifest] Getting manifest for app') + loggingStore.log(MESSAGE_TYPES.LOGGING, '[getManifest] Getting manifest for app') const manifestPath = join(fileLocation, 'manifest.json') if (!existsSync(manifestPath)) { throw new Error('manifest.json not found after extraction') @@ -39,7 +39,7 @@ export async function getManifest(fileLocation: string): Promise => { - console.log(`Sending message to clients: ${JSON.stringify(data)}`) + loggingStore.log(MESSAGE_TYPES.LOGGING, `Sending message to clients: ${JSON.stringify(data)}`) if (server) { server.clients.forEach((client) => { if (client.readyState === 1) { @@ -15,7 +15,7 @@ export const sendMessageToClients = async (data: SocketData): Promise => { } }) } else { - dataListener.emit(MESSAGE_TYPES.LOGGING, 'WSOCKET: No server running - setting one up') + loggingStore.log(MESSAGE_TYPES.LOGGING, 'WSOCKET: No server running - setting one up') } } @@ -25,10 +25,13 @@ export const disconnectClient = (connectionId: string): void => { if (client && server) { client.socket.terminate() Clients.splice(Clients.indexOf(client), 1) - console.log(`Forcibly disconnected client: ${connectionId}`) + loggingStore.log(MESSAGE_TYPES.LOGGING, `Forcibly disconnected client: ${connectionId}`) connectionsStore.removeClient(connectionId) } else { - console.log(`Client not found or server not running: ${connectionId}`) + loggingStore.log( + MESSAGE_TYPES.LOGGING, + `Client not found or server not running: ${connectionId}` + ) } } @@ -54,9 +57,11 @@ export const sendConfigData = async (clientId?: string): Promise => { try { const appData = await appState.getAllBase() - sendMessageToClient(clientId, { app: 'client', type: 'config', payload: appData }) + const filteredAppData = appData.filter((app) => app.manifest?.isWebApp !== false) - console.log('WSOCKET: Preferences sent!') + sendMessageToClient(clientId, { app: 'client', type: 'config', payload: filteredAppData }) + + loggingStore.log(MESSAGE_TYPES.LOGGING, 'WSOCKET: Preferences sent!') } catch (error) { console.error('WSOCKET: Error getting config data:', error) sendError(clientId, 'WSOCKET: Error getting config data') @@ -87,7 +92,7 @@ export const sendSettingsData = async (clientId?: string): Promise => { } sendMessageToClient(clientId, { app: 'client', type: 'settings', payload: settings }) - console.log('WSOCKET: Preferences sent!') + loggingStore.log(MESSAGE_TYPES.LOGGING, 'WSOCKET: Preferences sent!') } catch (error) { console.error('WSOCKET: Error getting config data:', error) sendError(clientId, 'WSOCKET: Error getting config data') @@ -100,8 +105,8 @@ export const sendMappings = async (clientId?: string): Promise => { sendMessageToClient(clientId, { app: 'client', type: 'button_mappings', payload: mappings }) - console.log('WSOCKET: Button mappings sent!') - dataListener.asyncEmit(MESSAGE_TYPES.LOGGING, `WEBSOCKET: Client has been sent button maps!`) + loggingStore.log(MESSAGE_TYPES.LOGGING, 'WSOCKET: Button mappings sent!') + loggingStore.log(MESSAGE_TYPES.LOGGING, `WEBSOCKET: Client has been sent button maps!`) } catch (error) { console.error('WSOCKET: Error getting button mappings:', error) sendError(clientId, 'WSOCKET: Error getting button mappings') diff --git a/DeskThingServer/src/main/services/client/clientUtils.ts b/DeskThingServer/src/main/services/client/clientUtils.ts index 67a4cf18..f84c6936 100644 --- a/DeskThingServer/src/main/services/client/clientUtils.ts +++ b/DeskThingServer/src/main/services/client/clientUtils.ts @@ -30,7 +30,6 @@ export const sendTime = async (): Promise => { const formattedMinutes = minutes < 10 ? '0' + minutes : minutes const time = `${formattedHours}:${formattedMinutes} ${ampm}` sendMessageToClients({ app: 'client', type: 'time', payload: time }) - console.log(time) } const initializeTimer = async (): Promise => { diff --git a/DeskThingServer/src/main/services/client/expressServer.ts b/DeskThingServer/src/main/services/client/expressServer.ts index 9e3da617..4b4101b5 100644 --- a/DeskThingServer/src/main/services/client/expressServer.ts +++ b/DeskThingServer/src/main/services/client/expressServer.ts @@ -1,4 +1,5 @@ -import dataListener, { MESSAGE_TYPES } from '../../utils/events' +import loggingStore from '../../stores/loggingStore' +import { MESSAGE_TYPES } from '@shared/types' import { app as electronApp } from 'electron' import { join } from 'path' import { getAppFilePath } from '../apps' @@ -24,47 +25,57 @@ export const setupExpressServer = async (expressApp: express.Application): Promi ): void => { const userDataPath = electronApp.getPath('userData') const webAppDir = join(userDataPath, 'webapp') - dataListener.asyncEmit(MESSAGE_TYPES.LOGGING, `WEBSOCKET: Serving ${appName} from ${webAppDir}`) + loggingStore.log(MESSAGE_TYPES.LOGGING, `WEBSOCKET: Serving ${appName} from ${webAppDir}`) const clientIp = req.hostname - console.log(`WEBSOCKET: Serving ${appName} from ${webAppDir} to ${clientIp}`) + loggingStore.log( + MESSAGE_TYPES.LOGGING, + `WEBSOCKET: Serving ${appName} from ${webAppDir} to ${clientIp}` + ) + try { + if (req.path.endsWith('manifest.js')) { + const manifestPath = join(webAppDir, 'manifest.js') + if (fs.existsSync(manifestPath)) { + let manifestContent = fs.readFileSync(manifestPath, 'utf8') - if (req.path.endsWith('manifest.js')) { - const manifestPath = join(webAppDir, 'manifest.js') - if (fs.existsSync(manifestPath)) { - let manifestContent = fs.readFileSync(manifestPath, 'utf8') + manifestContent = manifestContent.replace( + /"ip":\s*".*?"/, + `"ip": "${clientIp === '127.0.0.1' ? 'localhost' : clientIp}"` + ) - manifestContent = manifestContent.replace( - /"ip":\s*".*?"/, - `"ip": "${clientIp === '127.0.0.1' ? 'localhost' : clientIp}"` - ) - - res.type('application/javascript').send(manifestContent) + res.type('application/javascript').send(manifestContent) + } else { + res.status(404).send('Manifest not found') + } } else { - res.status(404).send('Manifest not found') + if (fs.existsSync(webAppDir)) { + express.static(webAppDir)(req, res, next) + } else { + res.status(404).send('App not found') + } } - } else { - if (fs.existsSync(webAppDir)) { - express.static(webAppDir)(req, res, next) + } catch (error) { + if (error instanceof Error) { + console.error('WEBSOCKET: Error serving app:', error.message) } else { - res.status(404).send('App not found') + console.error('WEBSOCKET: Error serving app:', error) } } } - expressApp.use('/', (req, res, next) => { + expressApp.use(['/', '/client/'], (req, res, next) => { handleClientConnection('client', req, res, next) }) // Serve web apps dynamically based on the URL - expressApp.use('/:appName', (req, res, next) => { + expressApp.use(['/app/:appName', '/:appName'], (req, res, next) => { const appName = req.params.appName if (appName === 'client' || appName == null) { handleClientConnection(appName, req, res, next) } else { const appPath = getAppFilePath(appName) - dataListener.asyncEmit(MESSAGE_TYPES.LOGGING, `WEBSOCKET: Serving ${appName} from ${appPath}`) + loggingStore.log(MESSAGE_TYPES.LOGGING, `WEBSOCKET: Serving ${appName} from ${appPath}`) if (fs.existsSync(appPath)) { express.static(appPath)(req, res, next) @@ -73,4 +84,62 @@ export const setupExpressServer = async (expressApp: express.Application): Promi } } }) + + // Serve icons dynamically based on the URL + expressApp.use('/icon/:appName/:iconName', (req, res, next) => { + const iconName = req.params.iconName + const appName = req.params.appName + if (iconName != null) { + const appPath = getAppFilePath(appName) + loggingStore.log(MESSAGE_TYPES.LOGGING, `WEBSOCKET: Serving ${appName} from ${appPath}`) + + if (fs.existsSync(join(appPath, 'icons', iconName))) { + express.static(join(appPath, 'icons'))(req, res, next) + } else { + res.status(404).send('Icon not found') + } + } + }) + + // Serve icons dynamically based on the URL + expressApp.use('/image/:appName/:imageName', (req, res, next) => { + const imageName = req.params.imageName + const appName = req.params.appName + if (imageName != null) { + const appPath = getAppFilePath(appName) + loggingStore.log(MESSAGE_TYPES.LOGGING, `WEBSOCKET: Serving ${appName} from ${appPath}`) + + if (fs.existsSync(join(appPath, 'images', imageName))) { + express.static(join(appPath, 'icons'))(req, res, next) + } else { + res.status(404).send('Icon not found') + } + } + }) + + // Proxy external resources + expressApp.use('/fetch/:url(*)', async (req, res) => { + try { + const url = decodeURIComponent(req.params.url) + loggingStore.log(MESSAGE_TYPES.LOGGING, `WEBSOCKET: Fetching external resource from ${url}`) + + const response = await fetch(url) + const contentType = response.headers.get('content-type') + + if (contentType) { + res.setHeader('Content-Type', contentType) + } + if (response.body) { + response.body.pipeTo(res) + } + } catch (error) { + if (error instanceof Error) { + loggingStore.log( + MESSAGE_TYPES.LOGGING, + `WEBSOCKET: Error fetching external resource: ${error.message}` + ) + } + res.status(500).send('Error fetching resource') + } + }) } diff --git a/DeskThingServer/src/main/services/client/websocket.ts b/DeskThingServer/src/main/services/client/websocket.ts index d5b1cca6..592ad86a 100644 --- a/DeskThingServer/src/main/services/client/websocket.ts +++ b/DeskThingServer/src/main/services/client/websocket.ts @@ -1,7 +1,14 @@ import WebSocket, { WebSocketServer } from 'ws' import { createServer, Server as HttpServer, IncomingMessage } from 'http' -import dataListener, { MESSAGE_TYPES } from '../../utils/events' -import { AppDataInterface, Client, ClientManifest, Settings, SocketData } from '@shared/types' +import loggingStore from '../../stores/loggingStore' +import { + AppDataInterface, + MESSAGE_TYPES, + Client, + ClientManifest, + Settings, + SocketData +} from '@shared/types' import { addData } from '../../handlers/dataHandler' import { HandleDeviceData } from '../../handlers/deviceHandler' import settingsStore from '../../stores/settingsStore' @@ -36,8 +43,8 @@ const THROTTLE_DELAY = 100 // milliseconds export const restartServer = async (): Promise => { try { if (server) { - console.log('WSOCKET: Shutting down the WebSocket server...') - dataListener.emit(MESSAGE_TYPES.LOGGING, 'WSOCKET: Shutting down the WebSocket server...') + loggingStore.log(MESSAGE_TYPES.LOGGING, 'WSOCKET: Shutting down the WebSocket server...') + loggingStore.log(MESSAGE_TYPES.LOGGING, 'WSOCKET: Shutting down the WebSocket server...') ConnectionStore.removeAllClients() server.clients.forEach((client) => { @@ -47,25 +54,28 @@ export const restartServer = async (): Promise => { server.close((err) => { if (err) { console.error('WSOCKET: Error shutting down WebSocket server:', err) - dataListener.emit( + loggingStore.log( MESSAGE_TYPES.ERROR, 'WSOCKET: Error shutting down WebSocket server:' + err ) } else { - console.log('WSOCKET: WebSocket server shut down successfully.') - dataListener.emit( + loggingStore.log( + MESSAGE_TYPES.LOGGING, + 'WSOCKET: WebSocket server shut down successfully.' + ) + loggingStore.log( MESSAGE_TYPES.LOGGING, 'WSOCKET: WebSocket server shut down successfully.' ) } if (httpServer && httpServer.listening) { - console.log('WSOCKET: Stopping HTTP server...') + loggingStore.log(MESSAGE_TYPES.LOGGING, 'WSOCKET: Stopping HTTP server...') httpServer.close((err) => { if (err) { console.error('WSOCKET: Error stopping HTTP server:', err) } else { - console.log('WSOCKET: HTTP server stopped successfully.') + loggingStore.log(MESSAGE_TYPES.LOGGING, 'WSOCKET: HTTP server stopped successfully.') setupServer() } }) @@ -74,14 +84,17 @@ export const restartServer = async (): Promise => { } }) } else { - console.log('WSOCKET: No WebSocket server running - setting one up') + loggingStore.log( + MESSAGE_TYPES.LOGGING, + 'WSOCKET: No WebSocket server running - setting one up' + ) if (httpServer && httpServer.listening) { - console.log('WSOCKET: Stopping HTTP server...') + loggingStore.log(MESSAGE_TYPES.LOGGING, 'WSOCKET: Stopping HTTP server...') httpServer.close((err) => { if (err) { console.error('WSOCKET: Error stopping HTTP server:', err) } else { - console.log('WSOCKET: HTTP server stopped successfully.') + loggingStore.log(MESSAGE_TYPES.LOGGING, 'WSOCKET: HTTP server stopped successfully.') setupServer() } }) @@ -95,7 +108,7 @@ export const restartServer = async (): Promise => { } export const setupServer = async (): Promise => { - dataListener.asyncEmit(MESSAGE_TYPES.MESSAGE, 'WSOCKET: Attempting to setup the server') + loggingStore.log(MESSAGE_TYPES.MESSAGE, 'WSOCKET: Attempting to setup the server') if (!currentPort || !currentAddress) { const settings = await settingsStore.getSettings() @@ -112,11 +125,10 @@ export const setupServer = async (): Promise => { server = new WebSocketServer({ server: httpServer }) - console.log('WSOCKET: WebSocket server is running.') + loggingStore.log(MESSAGE_TYPES.LOGGING, 'WSOCKET: WebSocket server is running.') httpServer.listen(currentPort, currentAddress, () => { - console.log(`CALLBACK: Server listening on ${currentAddress}:${currentPort}`) - dataListener.asyncEmit( + loggingStore.log( MESSAGE_TYPES.LOGGING, `WEBSOCKET: Server is listening on ${currentAddress}:${currentPort}` ) @@ -130,7 +142,10 @@ export const setupServer = async (): Promise => { const clientIp = socket._socket.remoteAddress // Setup the initial client data - console.log(`WSOCKET: Client connected! Looking for client with IP ${clientIp}`) + loggingStore.log( + MESSAGE_TYPES.LOGGING, + `WSOCKET: Client connected! Looking for client with IP ${clientIp}` + ) // Local client that is the true source of truth for the device details const client: Client = { @@ -145,13 +160,17 @@ export const setupServer = async (): Promise => { Clients.push({ client, socket }) - console.log( + loggingStore.log( + MESSAGE_TYPES.LOGGING, `WSOCKET: Client with id: ${client.connectionId} connected!\nWSOCKET: Sending preferences...` ) ConnectionStore.addClient(client) - console.log('WSOCKET: Client connected!\nWSOCKET: Sending preferences...') - dataListener.asyncEmit(MESSAGE_TYPES.LOGGING, `WEBSOCKET: Sending client preferences...`) + loggingStore.log( + MESSAGE_TYPES.LOGGING, + 'WSOCKET: Client connected!\nWSOCKET: Sending preferences...' + ) + loggingStore.log(MESSAGE_TYPES.LOGGING, `WEBSOCKET: Sending client preferences...`) sendConfigData(client.connectionId) sendSettingsData(client.connectionId) @@ -160,8 +179,7 @@ export const setupServer = async (): Promise => { socket.on('message', async (message) => { const messageData = JSON.parse(message) as SocketData - console.log(`WSOCKET: ${client.connectionId} sent message `, messageData) - dataListener.asyncEmit( + loggingStore.log( MESSAGE_TYPES.LOGGING, `WEBSOCKET: Client ${client.connectionId} has sent ${message}` ) @@ -207,7 +225,7 @@ export const setupServer = async (): Promise => { }) socket.on('close', () => { - dataListener.asyncEmit( + loggingStore.log( MESSAGE_TYPES.LOGGING, `WSOCKET: Client ${client.connectionId} has disconnected!` ) @@ -223,7 +241,10 @@ export const setupServer = async (): Promise => { const handleServerMessage = (socket, client: Client, messageData: SocketData): void => { try { if (messageData.app === 'server') { - console.log(`WSOCKET: Server message received! ${messageData.type}`) + loggingStore.log( + MESSAGE_TYPES.LOGGING, + `WSOCKET: Server message received! ${messageData.type}` + ) try { switch (messageData.type) { case 'preferences': @@ -241,7 +262,7 @@ const handleServerMessage = (socket, client: Client, messageData: SocketData): v ) break case 'pong': - console.log('Received pong from ', client.connectionId) + loggingStore.log(MESSAGE_TYPES.LOGGING, 'Received pong from ', client.connectionId) sendIpcData(`pong-${client.connectionId}`, messageData.payload) break case 'set': @@ -266,7 +287,7 @@ const handleServerMessage = (socket, client: Client, messageData: SocketData): v sendTime() break case 'message': - dataListener.asyncEmit( + loggingStore.log( MESSAGE_TYPES.MESSAGE, `${client.connectionId}: ${messageData.payload}` ) @@ -279,7 +300,10 @@ const handleServerMessage = (socket, client: Client, messageData: SocketData): v const manifest = messageData.payload as ClientManifest if (!manifest) return - console.log('WSOCKET: Received manifest from client', manifest) + loggingStore.log( + MESSAGE_TYPES.LOGGING, + 'WSOCKET: Received manifest from client' + JSON.stringify(manifest) + ) // Update the client to the info received from the client @@ -313,10 +337,10 @@ const handleServerMessage = (socket, client: Client, messageData: SocketData): v } } -dataListener.on(MESSAGE_TYPES.SETTINGS, (newSettings: Settings) => { +settingsStore.addListener((newSettings: Settings) => { if (currentPort !== newSettings.devicePort || currentAddress !== newSettings.address) { restartServer() } else { - dataListener.emit(MESSAGE_TYPES.LOGGING, 'WSOCKET: No settings changed!') + loggingStore.log(MESSAGE_TYPES.LOGGING, 'WSOCKET: No settings changed!') } }) diff --git a/DeskThingServer/src/main/static/defaultMapping.ts b/DeskThingServer/src/main/static/defaultMapping.ts index c7a44633..46e2244c 100644 --- a/DeskThingServer/src/main/static/defaultMapping.ts +++ b/DeskThingServer/src/main/static/defaultMapping.ts @@ -1,4 +1,4 @@ -import { Key, EventFlavor, Action, MappingStructure, ButtonMapping } from '@shared/types' +import { Key, EventMode, Action, MappingStructure, ButtonMapping } from '@shared/types' const keys: Key[] = [ { @@ -7,7 +7,7 @@ const keys: Key[] = [ description: 'First dynamic action button on the miniplayer', version: '0.9.0', enabled: true, - flavors: [EventFlavor.KeyDown] + Modes: [EventMode.KeyDown] }, { id: 'DynamicAction2', @@ -15,7 +15,7 @@ const keys: Key[] = [ description: 'Second dynamic action button on the miniplayer', version: '0.9.0', enabled: true, - flavors: [EventFlavor.KeyDown] + Modes: [EventMode.KeyDown] }, { id: 'DynamicAction3', @@ -23,7 +23,7 @@ const keys: Key[] = [ description: 'Third dynamic action button on the miniplayer', version: '0.9.0', enabled: true, - flavors: [EventFlavor.KeyDown] + Modes: [EventMode.KeyDown] }, { id: 'DynamicAction4', @@ -31,7 +31,7 @@ const keys: Key[] = [ description: 'Fourth dynamic action button on the miniplayer', version: '0.9.0', enabled: true, - flavors: [EventFlavor.KeyDown] + Modes: [EventMode.KeyDown] }, { id: 'Action5', @@ -39,7 +39,7 @@ const keys: Key[] = [ description: 'Fifth action button, always visible on the miniplayer', version: '0.9.0', enabled: true, - flavors: [EventFlavor.KeyDown] + Modes: [EventMode.KeyDown] }, { id: 'Action6', @@ -47,7 +47,7 @@ const keys: Key[] = [ description: 'Sixth action button, always visible on the miniplayer', version: '0.9.0', enabled: true, - flavors: [EventFlavor.KeyDown] + Modes: [EventMode.KeyDown] }, { id: 'Action7', @@ -55,7 +55,7 @@ const keys: Key[] = [ description: 'Seventh action button, always visible on the miniplayer', version: '0.9.0', enabled: true, - flavors: [EventFlavor.KeyDown] + Modes: [EventMode.KeyDown] }, { id: 'Digit1', @@ -63,7 +63,7 @@ const keys: Key[] = [ description: 'Physical Button Digit1', version: '0.9.0', enabled: true, - flavors: [EventFlavor.PressShort, EventFlavor.PressLong, EventFlavor.KeyDown, EventFlavor.KeyUp] + Modes: [EventMode.PressShort, EventMode.PressLong, EventMode.KeyDown, EventMode.KeyUp] }, { id: 'Digit2', @@ -71,7 +71,7 @@ const keys: Key[] = [ description: 'Physical Button Digit2', version: '0.9.0', enabled: true, - flavors: [EventFlavor.PressShort, EventFlavor.PressLong, EventFlavor.KeyDown, EventFlavor.KeyUp] + Modes: [EventMode.PressShort, EventMode.PressLong, EventMode.KeyDown, EventMode.KeyUp] }, { id: 'Digit3', @@ -79,7 +79,7 @@ const keys: Key[] = [ description: 'Physical Button Digit3', version: '0.9.0', enabled: true, - flavors: [EventFlavor.PressShort, EventFlavor.PressLong, EventFlavor.KeyDown, EventFlavor.KeyUp] + Modes: [EventMode.PressShort, EventMode.PressLong, EventMode.KeyDown, EventMode.KeyUp] }, { id: 'Digit4', @@ -87,7 +87,7 @@ const keys: Key[] = [ description: 'Physical Button Digit4', version: '0.9.0', enabled: true, - flavors: [EventFlavor.PressShort, EventFlavor.PressLong, EventFlavor.KeyDown, EventFlavor.KeyUp] + Modes: [EventMode.PressShort, EventMode.PressLong, EventMode.KeyDown, EventMode.KeyUp] }, { id: 'KeyM', @@ -95,7 +95,7 @@ const keys: Key[] = [ description: 'Physical Button M', version: '0.9.0', enabled: true, - flavors: [EventFlavor.PressShort, EventFlavor.PressLong, EventFlavor.KeyDown, EventFlavor.KeyUp] + Modes: [EventMode.PressShort, EventMode.PressLong, EventMode.KeyDown, EventMode.KeyUp] }, { id: 'Scroll', @@ -103,12 +103,7 @@ const keys: Key[] = [ description: 'Physical Button Scroll', version: '0.9.0', enabled: true, - flavors: [ - EventFlavor.ScrollUp, - EventFlavor.ScrollDown, - EventFlavor.ScrollLeft, - EventFlavor.ScrollRight - ] + Modes: [EventMode.ScrollUp, EventMode.ScrollDown, EventMode.ScrollLeft, EventMode.ScrollRight] }, { id: 'Enter', @@ -116,7 +111,7 @@ const keys: Key[] = [ description: 'Physical Button Enter', version: '0.9.0', enabled: true, - flavors: [EventFlavor.KeyDown, EventFlavor.PressLong, EventFlavor.KeyDown, EventFlavor.KeyUp] + Modes: [EventMode.KeyDown, EventMode.PressLong, EventMode.KeyDown, EventMode.KeyUp] }, { id: 'Escape', @@ -124,7 +119,7 @@ const keys: Key[] = [ description: 'Physical Button Escape', version: '0.9.0', enabled: true, - flavors: [EventFlavor.PressShort, EventFlavor.PressLong, EventFlavor.KeyDown, EventFlavor.KeyUp] + Modes: [EventMode.PressShort, EventMode.PressLong, EventMode.KeyDown, EventMode.KeyUp] }, { id: 'Swipe', @@ -132,12 +127,7 @@ const keys: Key[] = [ description: 'Touchpad Swipe Button', version: '0.9.0', enabled: true, - flavors: [ - EventFlavor.ScrollUp, - EventFlavor.ScrollDown, - EventFlavor.ScrollLeft, - EventFlavor.ScrollRight - ] + Modes: [EventMode.ScrollUp, EventMode.ScrollDown, EventMode.ScrollLeft, EventMode.ScrollRight] }, { id: 'Pad1', @@ -145,7 +135,7 @@ const keys: Key[] = [ description: 'Touch Pad 1 on the fullscreen miniplayer view', version: '0.9.0', enabled: true, - flavors: [EventFlavor.KeyDown] + Modes: [EventMode.KeyDown] }, { id: 'Pad2', @@ -153,7 +143,7 @@ const keys: Key[] = [ description: 'Touch Pad 2 on the fullscreen miniplayer view', version: '0.9.0', enabled: true, - flavors: [EventFlavor.KeyDown] + Modes: [EventMode.KeyDown] }, { id: 'Pad3', @@ -161,7 +151,7 @@ const keys: Key[] = [ description: 'Touch Pad 3 on the fullscreen miniplayer view', version: '0.9.0', enabled: true, - flavors: [EventFlavor.KeyDown] + Modes: [EventMode.KeyDown] }, { id: 'Pad4', @@ -169,7 +159,7 @@ const keys: Key[] = [ description: 'Touch Pad 4 on the fullscreen miniplayer view', version: '0.9.0', enabled: true, - flavors: [EventFlavor.KeyDown] + Modes: [EventMode.KeyDown] }, { id: 'Pad5', @@ -177,7 +167,7 @@ const keys: Key[] = [ description: 'Touch Pad 5 on the fullscreen miniplayer view', version: '0.9.0', enabled: true, - flavors: [EventFlavor.KeyDown] + Modes: [EventMode.KeyDown] }, { id: 'Pad6', @@ -185,7 +175,7 @@ const keys: Key[] = [ description: 'Touch Pad 6 on the fullscreen miniplayer view', version: '0.9.0', enabled: true, - flavors: [EventFlavor.KeyDown] + Modes: [EventMode.KeyDown] }, { id: 'Pad7', @@ -193,7 +183,7 @@ const keys: Key[] = [ description: 'Touch Pad 7 on the fullscreen miniplayer view', version: '0.9.0', enabled: true, - flavors: [EventFlavor.KeyDown] + Modes: [EventMode.KeyDown] }, { id: 'Pad8', @@ -201,7 +191,7 @@ const keys: Key[] = [ description: 'Touch Pad 8 on the fullscreen miniplayer view', version: '0.9.0', enabled: true, - flavors: [EventFlavor.KeyDown] + Modes: [EventMode.KeyDown] }, { id: 'Pad9', @@ -209,7 +199,7 @@ const keys: Key[] = [ description: 'Touch Pad 9 on the fullscreen miniplayer view', version: '0.9.0', enabled: true, - flavors: [EventFlavor.KeyDown] + Modes: [EventMode.KeyDown] } ] @@ -364,7 +354,7 @@ const defaults: ButtonMapping = { version: '0.9.0', mapping: { Pad1: { - [EventFlavor.KeyDown]: { + [EventMode.KeyDown]: { name: 'Volume Up', id: 'volUp', value: '15', @@ -376,7 +366,7 @@ const defaults: ButtonMapping = { } }, Pad2: { - [EventFlavor.KeyDown]: { + [EventMode.KeyDown]: { name: 'Open Previous', id: 'swipeL', description: 'Opens the app at the previous index', @@ -386,7 +376,7 @@ const defaults: ButtonMapping = { } }, Pad3: { - [EventFlavor.KeyDown]: { + [EventMode.KeyDown]: { name: 'Open Next', id: 'swipeR', description: 'Opens the app at the next index', @@ -396,7 +386,7 @@ const defaults: ButtonMapping = { } }, Pad4: { - [EventFlavor.KeyDown]: { + [EventMode.KeyDown]: { name: 'Volume Down', id: 'volDown', value: '15', @@ -408,7 +398,7 @@ const defaults: ButtonMapping = { } }, Pad5: { - [EventFlavor.KeyDown]: { + [EventMode.KeyDown]: { name: 'Toggle AppsList', id: 'appsList', value: 'hide', @@ -421,7 +411,7 @@ const defaults: ButtonMapping = { } }, Pad6: { - [EventFlavor.KeyDown]: { + [EventMode.KeyDown]: { name: 'Toggle AppsList', id: 'appsList', value: 'show', @@ -434,7 +424,7 @@ const defaults: ButtonMapping = { } }, Pad7: { - [EventFlavor.KeyDown]: { + [EventMode.KeyDown]: { name: 'Repeat', id: 'repeat', description: 'Toggles repeat', @@ -444,7 +434,7 @@ const defaults: ButtonMapping = { } }, Pad8: { - [EventFlavor.KeyDown]: { + [EventMode.KeyDown]: { name: 'PlayPause', id: 'play', icon: 'play', @@ -455,7 +445,7 @@ const defaults: ButtonMapping = { } }, Pad9: { - [EventFlavor.KeyDown]: { + [EventMode.KeyDown]: { name: 'Fullscreen', id: 'fullscreen', description: 'Toggles Fullscreen on most devices', @@ -465,7 +455,7 @@ const defaults: ButtonMapping = { } }, DynamicAction1: { - [EventFlavor.KeyDown]: { + [EventMode.KeyDown]: { name: 'Shuffle', id: 'shuffle', value: 'toggle', @@ -478,7 +468,7 @@ const defaults: ButtonMapping = { } }, DynamicAction2: { - [EventFlavor.KeyDown]: { + [EventMode.KeyDown]: { name: 'Repeat', id: 'repeat', description: 'Repeats the song', @@ -488,7 +478,7 @@ const defaults: ButtonMapping = { } }, DynamicAction3: { - [EventFlavor.KeyDown]: { + [EventMode.KeyDown]: { name: 'Rewind', id: 'rewind', value: 'stop', @@ -502,7 +492,7 @@ const defaults: ButtonMapping = { } }, DynamicAction4: { - [EventFlavor.KeyDown]: { + [EventMode.KeyDown]: { name: 'Hidden Button', id: 'hidden', description: 'Hides the button. Has no action', @@ -512,7 +502,7 @@ const defaults: ButtonMapping = { } }, Action5: { - [EventFlavor.KeyDown]: { + [EventMode.KeyDown]: { name: 'Hidden Button', id: 'hidden', description: 'Hides the button. Has no action', @@ -522,7 +512,7 @@ const defaults: ButtonMapping = { } }, Action6: { - [EventFlavor.KeyDown]: { + [EventMode.KeyDown]: { name: 'PlayPause', id: 'play', icon: 'play', @@ -533,7 +523,7 @@ const defaults: ButtonMapping = { } }, Action7: { - [EventFlavor.KeyDown]: { + [EventMode.KeyDown]: { name: 'Skip', id: 'skip', description: 'Skips the song', @@ -543,7 +533,7 @@ const defaults: ButtonMapping = { } }, Digit1: { - [EventFlavor.PressShort]: { + [EventMode.PressShort]: { name: 'Open Preference App', id: 'pref', value: '0', @@ -553,7 +543,7 @@ const defaults: ButtonMapping = { version: '0.9.0', enabled: true }, - [EventFlavor.PressLong]: { + [EventMode.PressLong]: { name: 'Swap Apps', id: 'swap', value: '0', @@ -565,7 +555,7 @@ const defaults: ButtonMapping = { } }, Digit2: { - [EventFlavor.PressShort]: { + [EventMode.PressShort]: { name: 'Open Preference App', id: 'pref', value: '1', @@ -575,7 +565,7 @@ const defaults: ButtonMapping = { version: '0.9.0', enabled: true }, - [EventFlavor.PressLong]: { + [EventMode.PressLong]: { name: 'Swap Apps', id: 'swap', value: '1', @@ -587,7 +577,7 @@ const defaults: ButtonMapping = { } }, Digit3: { - [EventFlavor.PressShort]: { + [EventMode.PressShort]: { name: 'Open Preference App', id: 'pref', value: '2', @@ -597,7 +587,7 @@ const defaults: ButtonMapping = { version: '0.9.0', enabled: true }, - [EventFlavor.PressLong]: { + [EventMode.PressLong]: { name: 'Swap Apps', id: 'swap', value: '2', @@ -609,7 +599,7 @@ const defaults: ButtonMapping = { } }, Digit4: { - [EventFlavor.PressShort]: { + [EventMode.PressShort]: { name: 'Open Preference App', id: 'pref', value: '3', @@ -619,7 +609,7 @@ const defaults: ButtonMapping = { version: '0.9.0', enabled: true }, - [EventFlavor.PressLong]: { + [EventMode.PressLong]: { name: 'Swap Apps', id: 'swap', value: '3', @@ -631,7 +621,7 @@ const defaults: ButtonMapping = { } }, KeyM: { - [EventFlavor.PressShort]: { + [EventMode.PressShort]: { name: 'Open App', id: 'open', value: 'dashboard', @@ -641,7 +631,7 @@ const defaults: ButtonMapping = { version: '0.9.0', enabled: true }, - [EventFlavor.PressLong]: { + [EventMode.PressLong]: { name: 'Open App', id: 'open', value: 'utility', @@ -653,7 +643,7 @@ const defaults: ButtonMapping = { } }, Scroll: { - [EventFlavor.ScrollRight]: { + [EventMode.ScrollRight]: { name: 'Volume Up', id: 'volUp', value: '15', @@ -663,7 +653,7 @@ const defaults: ButtonMapping = { version: '0.9.0', enabled: true }, - [EventFlavor.ScrollUp]: { + [EventMode.ScrollUp]: { name: 'Volume Up', id: 'volUp', value: '15', @@ -673,7 +663,7 @@ const defaults: ButtonMapping = { version: '0.9.0', enabled: true }, - [EventFlavor.ScrollLeft]: { + [EventMode.ScrollLeft]: { name: 'Volume Down', id: 'volDown', value: '15', @@ -683,7 +673,7 @@ const defaults: ButtonMapping = { version: '0.9.0', enabled: true }, - [EventFlavor.ScrollDown]: { + [EventMode.ScrollDown]: { name: 'Volume Down', id: 'volDown', value: '15', @@ -695,7 +685,7 @@ const defaults: ButtonMapping = { } }, Enter: { - [EventFlavor.KeyDown]: { + [EventMode.KeyDown]: { name: 'PlayPause', id: 'play', icon: 'play', @@ -704,7 +694,7 @@ const defaults: ButtonMapping = { version: '0.9.0', enabled: true }, - [EventFlavor.PressLong]: { + [EventMode.PressLong]: { name: 'Skip', id: 'skip', description: 'Skips the song', @@ -714,7 +704,7 @@ const defaults: ButtonMapping = { } }, Escape: { - [EventFlavor.PressShort]: { + [EventMode.PressShort]: { name: 'Toggle AppsList', id: 'appsList', value: 'show', @@ -725,7 +715,7 @@ const defaults: ButtonMapping = { version: '0.9.0', enabled: true }, - [EventFlavor.PressLong]: { + [EventMode.PressLong]: { name: 'Toggle AppsList', id: 'appsList', value: 'hide', @@ -738,7 +728,7 @@ const defaults: ButtonMapping = { } }, Swipe: { - [EventFlavor.SwipeUp]: { + [EventMode.SwipeUp]: { name: 'Toggle AppsList', id: 'appsList', value: 'hide', @@ -748,7 +738,7 @@ const defaults: ButtonMapping = { version: '0.9.0', enabled: true }, - [EventFlavor.SwipeDown]: { + [EventMode.SwipeDown]: { name: 'Toggle AppsList', id: 'appsList', value: 'show', @@ -758,7 +748,7 @@ const defaults: ButtonMapping = { version: '0.9.0', enabled: true }, - [EventFlavor.SwipeLeft]: { + [EventMode.SwipeLeft]: { name: 'Open Previous', id: 'swipeL', description: 'Opens the app at the previous index', @@ -766,7 +756,7 @@ const defaults: ButtonMapping = { version: '0.9.0', enabled: true }, - [EventFlavor.SwipeRight]: { + [EventMode.SwipeRight]: { name: 'Open Next', id: 'swipeR', description: 'Opens the app at the next index', diff --git a/DeskThingServer/src/main/stores/connectionsStore.ts b/DeskThingServer/src/main/stores/connectionsStore.ts index d69aab86..3fcfc193 100644 --- a/DeskThingServer/src/main/stores/connectionsStore.ts +++ b/DeskThingServer/src/main/stores/connectionsStore.ts @@ -1,6 +1,6 @@ -import { Client } from '@shared/types' +import { Client, MESSAGE_TYPES } from '@shared/types' import { handleAdbCommands } from '../handlers/adbHandler' -import dataListener, { MESSAGE_TYPES } from '../utils/events' +import loggingStore from '../stores/loggingStore' import settingsStore from './settingsStore' type ClientListener = (client: Client[]) => void @@ -20,7 +20,7 @@ class ConnectionStore { this.autoDetectADB = settings.autoDetectADB }) - dataListener.on(MESSAGE_TYPES.SETTINGS, (newSettings) => { + settingsStore.addListener((newSettings) => { try { if (newSettings.autoDetectADB !== undefined) { this.autoDetectADB = newSettings.autoDetectADB @@ -31,14 +31,18 @@ class ConnectionStore { if (newSettings.autoDetectADB) { this.checkAutoDetectADB() - dataListener.asyncEmit(MESSAGE_TYPES.LOGGING, '[ADB]: Auto-Detect is Enabled') + loggingStore.log(MESSAGE_TYPES.LOGGING, '[ADB]: Auto-Detect is Enabled') } else { - console.log('Auto-detect ADB disabled') - dataListener.asyncEmit(MESSAGE_TYPES.LOGGING, '[ADB]: Auto-Detect is Disabled') + loggingStore.log(MESSAGE_TYPES.LOGGING, 'Auto-detect ADB disabled') + loggingStore.log(MESSAGE_TYPES.LOGGING, '[ADB]: Auto-Detect is Disabled') } } } catch (error) { - dataListener.asyncEmit(MESSAGE_TYPES.ERROR, 'ADB: Error updating with settings', error) + if (error instanceof Error) { + loggingStore.log(MESSAGE_TYPES.ERROR, 'ADB: Error updating with settings', error.message) + } else { + loggingStore.log(MESSAGE_TYPES.ERROR, 'ADB: Error updating with settings', String(error)) + } } }) @@ -70,7 +74,7 @@ class ConnectionStore { } pingClient(connectionId: string): boolean { - console.log('Pinging client:', connectionId) + loggingStore.log(MESSAGE_TYPES.LOGGING, 'Pinging client:', connectionId) const clientIndex = this.clients.findIndex((c) => c.connectionId === connectionId) console.error('PINGING CLIENTS NOT IMPLEMENTED YET') if (clientIndex !== -1) { @@ -81,41 +85,38 @@ class ConnectionStore { } getClients(): Client[] { - console.log('Getting clients:', this.clients) return this.clients } getDevices(): string[] { - console.log('Getting devices:', this.devices) return this.devices } addClient(client: Client): void { - console.log('Adding client:', client) this.clients.push(client) this.notifyListeners() } updateClient(connectionId: string, updates: Partial): void { - console.log('Updating client:', connectionId, updates) + loggingStore.log(MESSAGE_TYPES.LOGGING, 'Updating client:' + connectionId + updates) const clientIndex = this.clients.findIndex((c) => c.connectionId === connectionId) if (clientIndex !== -1) { this.clients[clientIndex] = { ...this.clients[clientIndex], ...updates } this.notifyListeners() } else { - console.log('Client not found:', connectionId) + loggingStore.log(MESSAGE_TYPES.LOGGING, 'Client not found:', connectionId) } } removeClient(connectionId: string): void { - console.log('Removing client:', connectionId) + loggingStore.log(MESSAGE_TYPES.LOGGING, 'Removing client:' + connectionId) this.clients = this.clients.filter((c) => c.connectionId !== connectionId) this.notifyListeners() } removeAllClients(): void { - console.log('Removing all clients') + loggingStore.log(MESSAGE_TYPES.LOGGING, 'Removing all clients') this.clients = [] this.notifyListeners() } @@ -142,7 +143,7 @@ class ConnectionStore { const newDevices = parseADBDevices(result) || [] this.devices = newDevices this.notifyDeviceListeners() - dataListener.asyncEmit(MESSAGE_TYPES.LOGGING, 'ADB Device found!') + loggingStore.log(MESSAGE_TYPES.LOGGING, 'ADB Device found!') return newDevices }) .catch((error) => { @@ -158,7 +159,7 @@ class ConnectionStore { const checkAndAutoDetect = async (): Promise => { if (this.autoDetectADB === true) { - console.log('Auto-detecting ADB devices...') + loggingStore.log(MESSAGE_TYPES.LOGGING, 'Auto-detecting ADB devices...') await this.getAdbDevices() this.clearTimeout = await setTimeout(checkAndAutoDetect, 7000) } diff --git a/DeskThingServer/src/main/stores/keyMapStore.ts b/DeskThingServer/src/main/stores/keyMapStore.ts index 8caa4a3d..b3ccb417 100644 --- a/DeskThingServer/src/main/stores/keyMapStore.ts +++ b/DeskThingServer/src/main/stores/keyMapStore.ts @@ -1,6 +1,13 @@ import { defaultData } from '../static/defaultMapping' -import { Action, ButtonMapping, EventFlavor, Key, MappingStructure } from '@shared/types' -import dataListener, { MESSAGE_TYPES } from '../utils/events' +import { + Action, + ButtonMapping, + MESSAGE_TYPES, + EventMode, + Key, + MappingStructure +} from '@shared/types' +import loggingStore from '../stores/loggingStore' import { readFromFile, readFromGlobalFile, @@ -50,7 +57,7 @@ export class MappingState { private loadMappings(): MappingStructure { const data = readFromFile('mappings.json') as MappingStructure if (!data || data?.version !== defaultData.version) { - dataListener.asyncEmit( + loggingStore.log( MESSAGE_TYPES.ERROR, `MAPHANDLER: Mappings file is corrupt or does not exist, using default` ) @@ -59,7 +66,7 @@ export class MappingState { } const parsedData = data as MappingStructure if (!this.isValidFileStructure(parsedData)) { - dataListener.emit( + loggingStore.log( MESSAGE_TYPES.ERROR, `MAPHANDLER: Mappings file is corrupt, resetting to default` ) @@ -73,7 +80,7 @@ export class MappingState { if (this.isValidFileStructure(mapping)) { writeToFile(mapping, 'mappings.json') } else { - dataListener.emit( + loggingStore.log( MESSAGE_TYPES.ERROR, `MAPHANDLER: New Mappings file is corrupt, resetting to default` ) @@ -114,12 +121,12 @@ export class MappingState { */ isValidButtonMapping = (mapping: ButtonMapping): boolean => { try { - for (const [key, flavors] of Object.entries(mapping.mapping)) { + for (const [key, Modes] of Object.entries(mapping.mapping)) { if (typeof key !== 'string') return false - if (typeof flavors !== 'object') return false + if (typeof Modes !== 'object') return false - for (const [flavor, action] of Object.entries(flavors)) { - if (!Object.values(EventFlavor).includes(Number(flavor))) { + for (const [Mode, action] of Object.entries(Modes)) { + if (!Object.values(EventMode).includes(Number(Mode))) { return false } if ( @@ -174,8 +181,8 @@ export class MappingState { typeof key.source === 'string' && typeof key.version === 'string' && typeof key.enabled === 'boolean' && - Array.isArray(key.flavors) && - key.flavors.every((flavor) => Object.values(EventFlavor).includes(flavor)) + Array.isArray(key.Modes) && + key.Modes.every((Mode) => Object.values(EventMode).includes(Mode)) ) } @@ -189,18 +196,13 @@ export class MappingState { * adds a new button mapping to the mapping structure. If the key already exists, it will update the mapping. * @param DynamicAction2 - the button to add * @param key - The key to map the button to - * @param flavor - default is 'onPress' + * @param Mode - default is 'onPress' * @param profile - default is 'default' */ - addButton = ( - action: Action, - key: string, - flavor: EventFlavor, - profile: string = 'default' - ): void => { + addButton = (action: Action, key: string, Mode: EventMode, profile: string = 'default'): void => { const mappings = this.mappings if (!mappings[profile]) { - dataListener.asyncEmit( + loggingStore.log( MESSAGE_TYPES.ERROR, `MAPHANDLER: Profile ${profile} does not exist! Create a new profile with the name ${profile} and try again` ) @@ -211,7 +213,7 @@ export class MappingState { } // Ensure that the structure of the button is valid if (!this.isValidAction(action)) { - dataListener.emit( + loggingStore.log( MESSAGE_TYPES.ERROR, `MAPHANDLER: Action ${action.id} is invalid, cannot add to mapping` ) @@ -219,7 +221,7 @@ export class MappingState { } // Adding the button to the mapping - mappings[profile][key][flavor] = action + mappings[profile][key][Mode] = action // Save the mappings to file this.mappings = mappings @@ -228,13 +230,13 @@ export class MappingState { /** * Removes a button mapping from the mapping structure. * @param key - The key to remove the button from - * @param flavor - The flavor of the button to remove. Default removes all flavors + * @param Mode - The Mode of the button to remove. Default removes all Modes * @param profile - default is 'default' */ - removeButton = (key: string, flavor: EventFlavor | null, profile: string = 'default'): void => { + removeButton = (key: string, Mode: EventMode | null, profile: string = 'default'): void => { const mappings = this.mappings if (!mappings[profile]) { - dataListener.asyncEmit( + loggingStore.log( MESSAGE_TYPES.ERROR, `MAPHANDLER: Profile ${profile} does not exist! Create a new profile with the name ${profile} and try again` ) @@ -242,36 +244,36 @@ export class MappingState { } // Ensuring the key exists in the mapping if (!mappings[profile][key]) { - dataListener.asyncEmit( + loggingStore.log( MESSAGE_TYPES.ERROR, `MAPHANDLER: Key ${key} does not exist in profile ${profile}!` ) return } - if (flavor === null) { + if (Mode === null) { // Remove the entire key delete mappings[profile][key] - dataListener.asyncEmit( + loggingStore.log( MESSAGE_TYPES.LOGGING, `MAPHANDLER: Key ${key} removed from profile ${profile}` ) } else { - // Ensure that the flavor exists in the mapping - if (!mappings[profile][key][flavor]) { - dataListener.asyncEmit( + // Ensure that the Mode exists in the mapping + if (!mappings[profile][key][Mode]) { + loggingStore.log( MESSAGE_TYPES.ERROR, - `MAPHANDLER: Flavor ${flavor} does not exist in key ${key} in profile ${profile}!` + `MAPHANDLER: Mode ${Mode} does not exist in key ${key} in profile ${profile}!` ) } else { // Removing the button from the mapping - delete mappings[profile][key][flavor] + delete mappings[profile][key][Mode] } } // Save the mappings to file this.mappings = mappings - dataListener.asyncEmit( + loggingStore.log( MESSAGE_TYPES.LOGGING, `MAPHANDLER: Button ${key} removed from profile ${profile}` ) @@ -281,7 +283,7 @@ export class MappingState { const mappings = this.mappings // Validate key structure if (!this.isValidKey(key)) { - dataListener.asyncEmit(MESSAGE_TYPES.ERROR, `MAPHANDLER: Invalid key structure`) + loggingStore.log(MESSAGE_TYPES.ERROR, `MAPHANDLER: Invalid key structure`) return } // Check if the key already exists @@ -289,11 +291,11 @@ export class MappingState { if (existingKeyIndex !== -1) { // Replace the existing key mappings.keys[existingKeyIndex] = key - dataListener.asyncEmit(MESSAGE_TYPES.LOGGING, `MAPHANDLER: Key ${key.id} updated`) + loggingStore.log(MESSAGE_TYPES.LOGGING, `MAPHANDLER: Key ${key.id} updated`) } else { // Add the new key mappings.keys.push(key) - dataListener.asyncEmit(MESSAGE_TYPES.LOGGING, `MAPHANDLER: Key ${key.id} added`) + loggingStore.log(MESSAGE_TYPES.LOGGING, `MAPHANDLER: Key ${key.id} added`) } // Save the mappings this.mappings = mappings @@ -306,9 +308,9 @@ export class MappingState { if (keyIndex !== -1) { // Remove the key mappings.keys.splice(keyIndex, 1) - dataListener.asyncEmit(MESSAGE_TYPES.LOGGING, `MAPHANDLER: Key ${keyId} removed`) + loggingStore.log(MESSAGE_TYPES.LOGGING, `MAPHANDLER: Key ${keyId} removed`) } else { - dataListener.asyncEmit(MESSAGE_TYPES.ERROR, `MAPHANDLER: Key ${keyId} not found`) + loggingStore.log(MESSAGE_TYPES.ERROR, `MAPHANDLER: Key ${keyId} not found`) } // Save the mappings this.mappings = mappings @@ -323,7 +325,7 @@ export class MappingState { const mappings = this.mappings // Validate action structure if (!this.isValidAction(action)) { - dataListener.asyncEmit(MESSAGE_TYPES.ERROR, `MAPHANDLER: Invalid action structure`) + loggingStore.log(MESSAGE_TYPES.ERROR, `MAPHANDLER: Invalid action structure`) return } // Check if the action already exists @@ -331,11 +333,11 @@ export class MappingState { if (existingActionIndex !== -1) { // Replace the existing action mappings.actions[existingActionIndex] = action - dataListener.asyncEmit(MESSAGE_TYPES.LOGGING, `MAPHANDLER: Action ${action.id} updated`) + loggingStore.log(MESSAGE_TYPES.LOGGING, `MAPHANDLER: Action ${action.id} updated`) } else { // Add the new action mappings.actions.push(action) - dataListener.asyncEmit(MESSAGE_TYPES.LOGGING, `MAPHANDLER: Action ${action.id} added`) + loggingStore.log(MESSAGE_TYPES.LOGGING, `MAPHANDLER: Action ${action.id} added`) } // Save the mappings this.mappings = mappings @@ -348,9 +350,9 @@ export class MappingState { if (actionIndex !== -1) { // Remove the action mappings.actions.splice(actionIndex, 1) - dataListener.asyncEmit(MESSAGE_TYPES.LOGGING, `MAPHANDLER: Action ${actionId} removed`) + loggingStore.log(MESSAGE_TYPES.LOGGING, `MAPHANDLER: Action ${actionId} removed`) } else { - dataListener.asyncEmit(MESSAGE_TYPES.ERROR, `MAPHANDLER: Action ${actionId} not found`) + loggingStore.log(MESSAGE_TYPES.ERROR, `MAPHANDLER: Action ${actionId} not found`) } // Save the mappings this.mappings = mappings @@ -385,7 +387,7 @@ export class MappingState { // Remove keys with the specified source mappings.keys = mappings.keys.filter((key) => key.source !== sourceId) - dataListener.asyncEmit( + loggingStore.log( MESSAGE_TYPES.LOGGING, `MAPHANDLER: Actions for source ${sourceId} disabled in all profiles, global actions, and keys` ) @@ -427,7 +429,7 @@ export class MappingState { } }) - dataListener.asyncEmit( + loggingStore.log( MESSAGE_TYPES.LOGGING, `MAPHANDLER: Actions for source ${sourceId} disabled in all profiles, global actions, and keys` ) @@ -443,20 +445,17 @@ export class MappingState { if (actionIndex !== -1) { // Update the icon mappings.actions[actionIndex].icon = icon - dataListener.asyncEmit( - MESSAGE_TYPES.LOGGING, - `MAPHANDLER: Icon for action ${actionId} updated` - ) + loggingStore.log(MESSAGE_TYPES.LOGGING, `MAPHANDLER: Icon for action ${actionId} updated`) } else { - dataListener.asyncEmit(MESSAGE_TYPES.ERROR, `MAPHANDLER: Action ${actionId} not found`) + loggingStore.log(MESSAGE_TYPES.ERROR, `MAPHANDLER: Action ${actionId} not found`) } // Update the icon for all actions inside the current profile const currentMap = mappings.selected_profile if (currentMap) { const currentMapActions = mappings.profiles[currentMap].mapping - Object.values(currentMapActions).forEach((buttonFlavors) => { - Object.values(buttonFlavors).forEach((action) => { + Object.values(currentMapActions).forEach((buttonModes) => { + Object.values(buttonModes).forEach((action) => { if (action && action.id === actionId) { action.icon = icon } @@ -476,7 +475,7 @@ export class MappingState { // Update the icon return mappings.actions[actionIndex] } else { - dataListener.asyncEmit(MESSAGE_TYPES.ERROR, `MAPHANDLER: Action ${actionId} not found`) + loggingStore.log(MESSAGE_TYPES.ERROR, `MAPHANDLER: Action ${actionId} not found`) return null } } @@ -492,7 +491,7 @@ export class MappingState { if (this.mappings.profiles[profile]) { this.mappings.selected_profile = profile } else { - dataListener.asyncEmit( + loggingStore.log( MESSAGE_TYPES.ERROR, `MAPHANDLER: Profile ${profile} does not exist! Create a new profile with the name ${profile} and try again` ) @@ -513,16 +512,13 @@ export class MappingState { // Check if the profile name already exists if (mappings.profiles[profileName]) { - dataListener.asyncEmit( - MESSAGE_TYPES.ERROR, - `MAPHANDLER: Profile "${profileName}" already exists!` - ) + loggingStore.log(MESSAGE_TYPES.ERROR, `MAPHANDLER: Profile "${profileName}" already exists!`) return } // Ensure the base profile exists if (!mappings.profiles[baseProfile]) { - dataListener.asyncEmit( + loggingStore.log( MESSAGE_TYPES.ERROR, `MAPHANDLER: Base profile "${baseProfile}" does not exist!` ) @@ -549,7 +545,7 @@ export class MappingState { // Save the updated mappings this.mappings = mappings - dataListener.asyncEmit( + loggingStore.log( MESSAGE_TYPES.LOGGING, `MAPHANDLER: Profile "${profileName}" added successfully.` ) @@ -564,19 +560,13 @@ export class MappingState { // Prevent removal of the default profile if (profileName === 'default') { - dataListener.asyncEmit( - MESSAGE_TYPES.ERROR, - `MAPHANDLER: The "default" profile cannot be removed.` - ) + loggingStore.log(MESSAGE_TYPES.ERROR, `MAPHANDLER: The "default" profile cannot be removed.`) return } // Check if the profile exists if (!mappings.profiles[profileName]) { - dataListener.asyncEmit( - MESSAGE_TYPES.ERROR, - `MAPHANDLER: Profile "${profileName}" does not exist!` - ) + loggingStore.log(MESSAGE_TYPES.ERROR, `MAPHANDLER: Profile "${profileName}" does not exist!`) return } @@ -586,7 +576,7 @@ export class MappingState { // If the removed profile was the selected profile, revert to default if (mappings.selected_profile === profileName) { mappings.selected_profile = 'default' - dataListener.asyncEmit( + loggingStore.log( MESSAGE_TYPES.LOGGING, `MAPHANDLER: Selected profile was removed. Reverted to "default" profile.` ) @@ -595,7 +585,7 @@ export class MappingState { // Save the updated mappings this.mappings = mappings - dataListener.asyncEmit( + loggingStore.log( MESSAGE_TYPES.LOGGING, `MAPHANDLER: Profile "${profileName}" removed successfully.` ) @@ -610,7 +600,7 @@ export class MappingState { const mappings = this.mappings if (!mappings.profiles[profile]) { - dataListener.asyncEmit( + loggingStore.log( MESSAGE_TYPES.ERROR, `MAPHANDLER: Profile ${profile} does not exist! Cannot export.` ) @@ -620,7 +610,7 @@ export class MappingState { const profileData = mappings.profiles[profile] writeToGlobalFile(profileData, filePath) - dataListener.asyncEmit( + loggingStore.log( MESSAGE_TYPES.LOGGING, `MAPHANDLER: Profile ${profile} exported to ${filePath}` ) @@ -638,7 +628,7 @@ export class MappingState { const profileData = readFromGlobalFile(filePath) if (!profileData) { - dataListener.asyncEmit( + loggingStore.log( MESSAGE_TYPES.ERROR, `MAPHANDLER: Failed to load profile data from ${filePath}` ) @@ -646,10 +636,7 @@ export class MappingState { } if (!this.isValidButtonMapping(profileData)) { - dataListener.asyncEmit( - MESSAGE_TYPES.ERROR, - `MAPHANDLER: Invalid profile data in file ${filePath}` - ) + loggingStore.log(MESSAGE_TYPES.ERROR, `MAPHANDLER: Invalid profile data in file ${filePath}`) return } @@ -657,7 +644,7 @@ export class MappingState { mappings.profiles[profileName] = profileData this.mappings = mappings - dataListener.asyncEmit( + loggingStore.log( MESSAGE_TYPES.LOGGING, `MAPHANDLER: Profile ${profileName} imported from ${filePath}` ) @@ -667,16 +654,13 @@ export class MappingState { const mappings = this.mappings const profile = mappings.profiles[profileName] if (!profile) { - dataListener.asyncEmit( - MESSAGE_TYPES.ERROR, - `MAPHANDLER: Profile ${profileName} does not exist!` - ) + loggingStore.log(MESSAGE_TYPES.ERROR, `MAPHANDLER: Profile ${profileName} does not exist!`) return } // Update the profile with the provided data deepMerge(profile, updatedProfile) this.mappings = mappings - dataListener.asyncEmit( + loggingStore.log( MESSAGE_TYPES.LOGGING, `MAPHANDLER: Profile ${profileName} updated successfully.` ) diff --git a/DeskThingServer/src/main/stores/loggingStore.ts b/DeskThingServer/src/main/stores/loggingStore.ts new file mode 100644 index 00000000..70e7f320 --- /dev/null +++ b/DeskThingServer/src/main/stores/loggingStore.ts @@ -0,0 +1,133 @@ +import fs from 'fs' +import { join } from 'path' +import { app } from 'electron' +import { MESSAGE_TYPES, Log, LOGGING_LEVEL, Settings, ReplyData, ReplyFn } from '@shared/types' +import settingsStore from './settingsStore' + +// LoggingStore configuration +const logFile = join(app.getPath('userData'), 'application.log.json') +const readableLogFile = join(app.getPath('userData'), 'readable.log') + +// Ensure log directory exists +const logDir = app.getPath('userData') +if (!fs.existsSync(logDir)) { + fs.mkdirSync(logDir, { recursive: true }) +} + +class LoggingStore { + private static instance: LoggingStore + private listeners: ((data: Log) => void)[] = [] + private logs: Log[] = [] + private logLevel: LOGGING_LEVEL = LOGGING_LEVEL.PRODUCTION + + private constructor() { + fs.writeFileSync(logFile, '[]') + fs.writeFileSync(readableLogFile, '') + + settingsStore.addListener(this.settingsStoreListener.bind(this)) + } + + private settingsStoreListener(settings: Settings): void { + this.logLevel = settings.logLevel + } + + // Singleton instance + public static getInstance(): LoggingStore { + if (!LoggingStore.instance) { + LoggingStore.instance = new LoggingStore() + } + return LoggingStore.instance + } + + // Log a message + async log(level: MESSAGE_TYPES, message: string, source: string = 'server'): Promise { + if ( + level === MESSAGE_TYPES.LOGGING && + source === 'server' && + this.logLevel != LOGGING_LEVEL.SYSTEM + ) { + return + } + + if (level === MESSAGE_TYPES.LOGGING && this.logLevel == LOGGING_LEVEL.PRODUCTION) { + return + } + + const timestamp = new Date().toISOString() + const trace = new Error().stack || '' + + const logData: Log = { + source: source, + type: level, + log: message, + trace: trace, + date: timestamp + } + + this.logs.push(logData) + this.notifyListeners(logData) + + const readableTimestamp = new Date(timestamp).toLocaleString() + const readableMessage = `[${readableTimestamp}] [${source}] ${level.toUpperCase()}: ${message}\n` + + console.log(readableMessage) + + // Write to log file as JSON array + return new Promise((resolve, reject) => { + fs.writeFile(logFile, JSON.stringify(this.logs, null, 2), (err) => { + if (err) { + console.error('Failed to write to log file:', err) + reject(err) + } + resolve() + }) + + fs.appendFile(readableLogFile, readableMessage, (err) => { + if (err) { + console.error('Failed to write to log file:', err) + reject(err) + } + resolve() + }) + }) + } + + notifyListeners(data: Log): void { + this.listeners.forEach((listener) => listener(data)) + } + + addListener(callback: (data: Log) => void): void { + this.listeners.push(callback) + } + + public async getLogs(): Promise { + return new Promise((resolve, reject) => { + if (!fs.existsSync(logFile)) { + resolve([]) + return + } + + fs.readFile(logFile, 'utf8', (err, data) => { + if (err) { + return reject(err) + } + try { + const logs = data ? JSON.parse(data) : [] + resolve(logs) + } catch (error) { + reject(error) + } + }) + }) + } +} + +export const ResponseLogger = (replyFn: ReplyFn): ReplyFn => { + return async (channel: string, reply: ReplyData): Promise => { + LoggingStore.getInstance().log(MESSAGE_TYPES.LOGGING, `[${channel}]: ${JSON.stringify(reply)}`) + + replyFn(channel, reply) + } +} + +export default LoggingStore.getInstance() diff --git a/DeskThingServer/src/main/stores/settingsStore.ts b/DeskThingServer/src/main/stores/settingsStore.ts index 2035c0ff..327b8848 100644 --- a/DeskThingServer/src/main/stores/settingsStore.ts +++ b/DeskThingServer/src/main/stores/settingsStore.ts @@ -1,15 +1,18 @@ import { readFromFile, writeToFile } from '../utils/fileHandler' -import dataListener, { MESSAGE_TYPES } from '../utils/events' +import loggingStore from './loggingStore' import os from 'os' -import { Settings } from '@shared/types' +import { LOGGING_LEVEL, Settings, MESSAGE_TYPES } from '@shared/types' const settingsVersion = '0.9.2' const version_code = 9.2 +type SettingsStoreListener = (settings: Settings) => void + class SettingsStore { private settings: Settings private settingsFilePath: string = 'settings.json' private static instance: SettingsStore + private listeners: SettingsStoreListener[] = [] constructor() { this.settings = this.getDefaultSettings() @@ -18,7 +21,7 @@ class SettingsStore { if (settings) { this.settings = settings as Settings this.settings.localIp = getLocalIpAddress() - dataListener.asyncEmit(MESSAGE_TYPES.SETTINGS, this.settings) + this.notifyListeners() } }) .catch((err) => { @@ -32,6 +35,16 @@ class SettingsStore { return SettingsStore.instance } + public addListener(listener: SettingsStoreListener): void { + this.listeners.push(listener) + } + + private async notifyListeners(): Promise { + this.listeners.forEach((listener) => { + listener(this.settings) + }) + } + public async getSettings(): Promise { if (this.settings) { return this.settings @@ -41,19 +54,23 @@ class SettingsStore { } } + /** + * Updates a specific setting and saves it to file + * @param key - The key of the setting to update + * @param value - The new value for the setting + */ public updateSetting(key: string, value: boolean | undefined | string | number | string[]): void { if (key === 'autoStart' && typeof value === 'boolean') { this.updateAutoLaunch(value) } this.settings[key] = value - dataListener.asyncEmit(MESSAGE_TYPES.SETTINGS, this.settings) this.saveSettings() } public async loadSettings(): Promise { try { const data = await readFromFile(this.settingsFilePath) - dataListener.asyncEmit(MESSAGE_TYPES.LOGGING, 'SETTINGS: Loaded settings!') + loggingStore.log(MESSAGE_TYPES.LOGGING, 'SETTINGS: Loaded settings!') if (!data || !data.version_code || data.version_code < version_code) { // File does not exist, create it with default settings @@ -66,6 +83,8 @@ class SettingsStore { await this.updateAutoLaunch(data.autoStart) } + this.notifyListeners() + return data } catch (err) { console.error('Error loading settings:', err) @@ -89,21 +108,31 @@ class SettingsStore { } } + /** + * Saves the current settings to file. Emits an update if settings are passed + * @param settings - Overrides the current settings with the passed settings if passed + */ public async saveSettings(settings?: Settings): Promise { try { if (settings) { this.settings = settings as Settings await writeToFile(this.settings, this.settingsFilePath) - dataListener.asyncEmit(MESSAGE_TYPES.SETTINGS, this.settings) - dataListener.asyncEmit(MESSAGE_TYPES.LOGGING, 'SETTINGS: Updated settings!') + console.log('SETTINGS: Updated settings!', this.settings) + loggingStore.log(MESSAGE_TYPES.LOGGING, 'SETTINGS: Updated settings!') } else { - dataListener.asyncEmit(MESSAGE_TYPES.LOGGING, 'SETTINGS: Invalid setting format!') + loggingStore.log(MESSAGE_TYPES.LOGGING, 'SETTINGS: Invalid setting format!') } + + this.notifyListeners() } catch (err) { console.error('Error saving settings:', err) } } + /** + * + * @returns Returns the default settings for the application + */ private getDefaultSettings(): Settings { return { version: settingsVersion, @@ -111,6 +140,7 @@ class SettingsStore { callbackPort: 8888, devicePort: 8891, address: '0.0.0.0', + LogLevel: LOGGING_LEVEL.PRODUCTION, autoStart: false, autoConfig: false, minimizeApp: true, diff --git a/DeskThingServer/src/main/utils/events.ts b/DeskThingServer/src/main/utils/events.ts deleted file mode 100644 index 1e1666b0..00000000 --- a/DeskThingServer/src/main/utils/events.ts +++ /dev/null @@ -1,81 +0,0 @@ -import EventEmitter from 'events' -import Logger from './logger' -import { ReplyData, ReplyFn, SocketData } from '@shared/types' - -/** - * The MESSAGE_TYPES object defines a set of constants that represent the different types of messages that can be sent or received in the application. - */ -export const MESSAGE_TYPES = { - ERROR: 'error', - LOGGING: 'log', - MESSAGE: 'message', - CONFIG: 'config', - SETTINGS: 'settings', - MAPPINGS: 'mapping' -} - -/** - * Events is a class that extends the EventEmitter class from the 'events' module. - */ -class Events extends EventEmitter { - constructor() { - super() - } - - /** - * Emits an event with associated data to all connected clients. - * - * @param event - The name of the event to emit. Should be one of the MESSAGE_TYPES defined in this file. - * @param data - The data to be sent along with the event. Can be of any type, but typically an object containing relevant information. - * @returns void - * - * Usage: - * 1. Import the Events instance from this file. - * 2. Call the method with the appropriate event type and data. - * - * @example - * - * import { events } from './events' - * - * events.emit(MESSAGE_TYPES.MESSAGE, { content: 'Hello, world!' }) - */ - async asyncEmit(event: string, ...data: (string | SocketData | unknown)[]): Promise { - console.log(`[${event}] `, data) - return new Promise((resolve) => { - setImmediate(() => { - // Ensure that two arguments are only emitted at once - if (Array.isArray(data) && data.length > 0 && typeof data[0] === 'string') { - this.emit(event, data.join(' ')) - } else { - this.emit(event, data[0]) - } - - if (event === MESSAGE_TYPES.ERROR) { - Logger.error(`[${event}]: ${JSON.stringify(data)}`) - } else if (event === MESSAGE_TYPES.LOGGING) { - Logger.info(`[${event}]: ${JSON.stringify(data)}`) - } else { - Logger.debug(`[${event}]: ${JSON.stringify(data)}`) - } - resolve() - }) - }) - } -} - -const dataListener = new Events() - -/** - * Handles and standardizes the way to reply to ipc handlers - * @param replyFn - * @returns - */ -export const ResponseLogger = (replyFn: ReplyFn): ReplyFn => { - return async (channel: string, reply: ReplyData): Promise => { - dataListener.asyncEmit(MESSAGE_TYPES.LOGGING, `[CHANNEL][${channel}]: ${JSON.stringify(reply)}`) - - replyFn(channel, reply) - } -} - -export default dataListener diff --git a/DeskThingServer/src/main/utils/logger.ts b/DeskThingServer/src/main/utils/logger.ts deleted file mode 100644 index b517135c..00000000 --- a/DeskThingServer/src/main/utils/logger.ts +++ /dev/null @@ -1,78 +0,0 @@ -import fs from 'fs' -import { join } from 'path' -import { app } from 'electron' - -// Define log levels -enum LogLevel { - INFO = 'info', - DEBUG = 'debug', - ERROR = 'error' -} - -// Logger configuration -const logFile = join(app.getPath('userData'), 'application.log') - -// Ensure log directory exists -const logDir = app.getPath('userData') -if (!fs.existsSync(logDir)) { - fs.mkdirSync(logDir, { recursive: true }) -} - -class Logger { - private static instance: Logger - - private constructor() { - fs.writeFileSync(logFile, '') - } - - // Singleton instance - public static getInstance(): Logger { - if (!Logger.instance) { - Logger.instance = new Logger() - } - return Logger.instance - } - - // Log a message - private async log(level: LogLevel, message: string): Promise { - const timestamp = new Date().toLocaleTimeString() - const logMessage = `[${timestamp}]: ${level.toUpperCase()} | ${message}` - - // Append to log file - fs.appendFile(logFile, logMessage + '\n', (err) => { - if (err) { - console.error('Failed to write to log file:', err) - } - }) - } - - // Info level logging - public async info(message: string): Promise { - this.log(LogLevel.INFO, message) - } - - // Debug level logging - public async debug(message: string): Promise { - this.log(LogLevel.DEBUG, message) - } - - // Error level logging - public async error(message: string): Promise { - this.log(LogLevel.ERROR, message) - } - - public async getLogs(): Promise { - console.log('LOGGER: Getting logs') - return new Promise((resolve, reject) => { - fs.readFile(logFile, 'utf8', (err, data) => { - if (err) { - return reject(err) - } - const logs = data.trim().split('\n').filter(Boolean) - resolve(logs) - }) - }) - } -} - -export default Logger.getInstance() diff --git a/DeskThingServer/src/preload/index.d.ts b/DeskThingServer/src/preload/index.d.ts index f3e5bb1d..e6cf083d 100644 --- a/DeskThingServer/src/preload/index.d.ts +++ b/DeskThingServer/src/preload/index.d.ts @@ -1,5 +1,5 @@ import { ElectronAPI } from '@electron-toolkit/preload' -import { AppDataInterface, AppReturnData, Client, ClientManifest } from '@shared/types' +import { AppDataInterface, AppReturnData, Client, ClientManifest, Log } from '@shared/types' type AppData = { [key: string]: string } @@ -38,7 +38,7 @@ declare global { saveSettings: (settings: Settings) => Promise getSettings: () => Promise fetchGithub: (url: string) => Promise - getLogs: () => Promise + getLogs: () => Promise getMappings: () => Promise addProfile: (profile: string, baseProfile?: string) => Promise deleteProfile: (profile: string) => Promise diff --git a/DeskThingServer/src/preload/index.ts b/DeskThingServer/src/preload/index.ts index 5a38ef4c..7195ade0 100644 --- a/DeskThingServer/src/preload/index.ts +++ b/DeskThingServer/src/preload/index.ts @@ -11,6 +11,7 @@ import { IncomingData, IPC_HANDLERS, IPCData, + Log, Settings, SocketData } from '@shared/types' @@ -209,7 +210,7 @@ const api = { }) }, - getLogs: (): Promise => { + getLogs: (): Promise => { return sendCommand('UTILITY', { type: 'logs', request: 'get', diff --git a/DeskThingServer/src/renderer/src/assets/animations/Landing Animation-v2.json b/DeskThingServer/src/renderer/src/assets/animations/Landing Animation-v2.json index b6be43c4..07ef8fc7 100644 --- a/DeskThingServer/src/renderer/src/assets/animations/Landing Animation-v2.json +++ b/DeskThingServer/src/renderer/src/assets/animations/Landing Animation-v2.json @@ -1 +1 @@ -{"v":"5.9.0","fr":30,"ip":0,"op":60,"w":1230,"h":390,"nm":"LandingAnimation_Centered","ddd":0,"assets":[],"fonts":{"list":[{"fName":"Geist-Bold","fFamily":"Geist","fStyle":"Bold","ascent":70.9991455078125},{"fName":"GeistMono-Medium","fFamily":"Geist Mono","fStyle":"Medium","ascent":70.9991455078125}]},"layers":[{"ddd":0,"ind":1,"ty":3,"nm":"NULL 1","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":10,"s":[679.5,146.941,0],"to":[0,0,0],"ti":[0,0,0]},{"t":45,"s":[310.393,146.941,0]}],"ix":2,"l":2},"a":{"a":0,"k":[50,50,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":3,"nm":"NULL","parent":1,"sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.5],"y":[1]},"o":{"x":[0.33],"y":[0]},"t":0,"s":[0]},{"t":45,"s":[-360]}],"ix":10},"p":{"a":0,"k":[-65.299,89.341,0],"ix":2,"l":2},"a":{"a":0,"k":[50,50,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"ip":0,"op":150,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"InsideMask","parent":2,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50,50.002,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-51.694,0],[0,51.694],[51.694,0],[0,-51.694]],"o":[[51.694,0],[0,-51.694],[-51.694,0],[0,51.694]],"v":[[0,93.6],[93.6,0],[0,-93.6],[-93.6,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.117647059262,0.843137264252,0.376470595598,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":40,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.5],"y":[1]},"o":{"x":[0.33],"y":[0]},"t":0,"s":[0]},{"t":35,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 2","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":150,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Inside","parent":2,"tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[49.025,49.837,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[242,242,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-23.57],[-23.79,0],[0,23.57],[23.79,0]],"o":[[0,23.57],[23.79,0],[0,-23.57],[-23.79,0]],"v":[[-43.15,0],[0,42.75],[43.15,0],[0,-42.75]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,20.12],[-20.34,0],[0,-20.12],[20.34,0]],"o":[[0,-20.12],[20.34,0],[0,20.12],[-20.34,0]],"v":[[-36.89,0],[0,-36.49],[36.89,0],[0,36.49]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0.133333340287,0.772549033165,0.368627458811,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":150,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"OutsideMask","parent":2,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50,49.998,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-41.36,-133.505],[-31.87,-173.275],[33.27,-173.275],[41.13,-133.005],[65.5,-123.555],[99.81,-145.935],[145.97,-98.215],[123.8,-63.315],[133.25,-40.025],[174.08,-32.005],[174.08,33.405],[133.25,41.925],[123.58,65.195],[146.42,100.095],[99.63,145.815],[64.71,123.385],[41.23,132.975],[32.23,173.285],[-33.15,173.285],[-41.6,132.195],[-65.92,123.285],[-100.17,145.545],[-146.41,99.695],[-124.24,64.555],[-133.95,41.725],[-173.84,32.745],[-174.08,-32.045],[-133.95,-40.025],[-124.1,-64.105],[-146.73,-98.215],[-99.5,-145.935],[-64.2,-124.055]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":25,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.5],"y":[1]},"o":{"x":[0.33],"y":[0]},"t":0,"s":[0]},{"t":35,"s":[100]}],"ix":2},"o":{"a":0,"k":4,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 2","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":150,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Outside","parent":2,"tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[49.025,49.817,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[242,242,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-3.33,0],[0,0],[0,0],[-0.68,3.26],[0,0],[0,0],[0,0],[-2.36,2.36],[0,0],[1.83,2.79],[0,0],[0,0],[0,0],[0,3.33],[0,0],[3.26,0.68],[0,0],[0,0],[0,0],[2.36,2.36],[0,0],[2.79,-1.83],[0,0],[0,0],[0,0],[3.33,0],[0,0],[0.68,-3.26],[0,0],[0,0],[0,0],[2.36,-2.36],[0,0],[-1.83,-2.79],[0,0],[0,0],[0,0],[0,-3.33],[0,0],[-3.26,-0.68],[0,0],[0,0],[0,0],[-2.36,-2.36],[0,0],[-2.79,1.83],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[3.34,0],[0,0],[0,0],[0,0],[2.79,1.82],[0,0],[2.36,-2.36],[0,0],[0,0],[0,0],[3.26,-0.68],[0,0],[0,-3.34],[0,0],[0,0],[0,0],[1.82,-2.79],[0,0],[-2.35,-2.36],[0,0],[0,0],[0,0],[-0.68,-3.26],[0,0],[-3.34,0],[0,0],[0,0],[0,0],[-2.79,-1.82],[0,0],[-2.36,2.35],[0,0],[0,0],[0,0],[-3.26,0.68],[0,0],[0,3.34],[0,0],[0,0],[0,0],[-1.82,2.79],[0,0],[2.36,2.36],[0,0],[0,0],[0,0],[0.68,3.26]],"v":[[-11.25,76.14],[11.25,76.14],[11.25,76.12],[18.18,70.49],[20.75,58.17],[26.47,55.8],[37,62.69],[45.88,61.77],[61.79,45.86],[62.71,36.98],[55.82,26.46],[58.19,20.74],[70.51,18.17],[76.14,11.24],[76.14,-11.26],[70.51,-18.19],[58.19,-20.76],[55.82,-26.48],[62.71,-37],[61.79,-45.88],[45.88,-61.79],[37,-62.71],[26.47,-55.82],[20.75,-58.19],[18.18,-70.51],[11.25,-76.14],[-11.25,-76.14],[-18.18,-70.51],[-20.75,-58.19],[-26.47,-55.82],[-37,-62.71],[-45.88,-61.79],[-61.79,-45.88],[-62.71,-37],[-55.82,-26.47],[-58.19,-20.75],[-70.51,-18.18],[-76.14,-11.25],[-76.14,11.25],[-70.51,18.18],[-58.19,20.75],[-55.82,26.47],[-62.71,37],[-61.79,45.88],[-45.88,61.79],[-37,62.71],[-26.47,55.82],[-20.75,58.19],[-18.18,70.51]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[2.16,0.9],[0,0],[1.96,-1.28],[0,0],[0,0],[0,0],[0.9,2.17],[0,0],[2.29,0.48],[0,0],[0,0],[0,0],[-0.9,2.17],[0,0],[1.28,1.96],[0,0],[0,0],[0,0],[-2.17,0.9],[0,0],[-0.48,2.29],[0,0],[0,0],[0,0],[-2.17,-0.9],[0,0],[-1.96,1.28],[0,0],[0,0],[0,0],[-0.9,-2.17],[0,0],[-2.29,-0.48],[0,0],[0,0],[0,0],[0.9,-2.16],[0,0],[-1.28,-1.96],[0,0],[0,0],[0,0],[2.17,-0.9],[0,0],[0.48,-2.29]],"o":[[0,0],[0,0],[0,0],[-0.48,-2.29],[0,0],[-2.17,-0.89],[0,0],[0,0],[0,0],[1.29,-1.96],[0,0],[-0.89,-2.16],[0,0],[0,0],[0,0],[2.3,-0.48],[0,0],[0.9,-2.17],[0,0],[0,0],[0,0],[1.96,1.28],[0,0],[2.16,-0.89],[0,0],[0,0],[0,0],[0.48,2.3],[0,0],[2.17,0.9],[0,0],[0,0],[0,0],[-1.29,1.96],[0,0],[0.89,2.17],[0,0],[0,0],[0,0],[-2.29,0.48],[0,0],[-0.9,2.17],[0,0],[0,0],[0,0],[-1.96,-1.29],[0,0],[-2.17,0.89],[0,0]],"v":[[10.22,67.8],[-10.22,67.8],[-10.21,67.79],[-12.7,55.82],[-16.92,50.73],[-23.88,47.85],[-30.47,48.47],[-40.69,55.17],[-55.14,40.71],[-48.44,30.49],[-47.82,23.9],[-50.7,16.95],[-55.79,12.73],[-67.76,10.24],[-67.76,-10.2],[-55.8,-12.69],[-50.7,-16.91],[-47.82,-23.86],[-48.44,-30.45],[-55.14,-40.67],[-40.69,-55.13],[-30.47,-48.43],[-23.88,-47.81],[-16.92,-50.69],[-12.7,-55.78],[-10.21,-67.74],[10.23,-67.74],[12.72,-55.78],[16.94,-50.68],[23.89,-47.8],[30.48,-48.42],[40.7,-55.12],[55.15,-40.66],[48.45,-30.44],[47.83,-23.85],[50.71,-16.9],[55.8,-12.68],[67.77,-10.19],[67.77,10.25],[55.8,12.74],[50.71,16.96],[47.83,23.92],[48.45,30.51],[55.15,40.73],[40.69,55.18],[30.47,48.48],[23.88,47.86],[16.93,50.74],[12.71,55.83]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":150,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":5,"nm":"Version v0.9.0","cl":"9 0","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[165.298,-14,0],"ix":2,"l":2},"a":{"a":0,"k":[2.981,-22.559,0],"ix":1,"l":2},"s":{"a":0,"k":[134.638,134.638,100],"ix":6,"l":2}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"a","pt":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[1.088,-29.246],[1.088,0.645],[390.702,0.599],[390.702,-29.292]],"c":true},"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"Mask 1"}],"t":{"d":{"k":[{"s":{"s":40,"f":"GeistMono-Medium","t":"Version v0.9.0","ca":0,"j":0,"tr":0,"lh":94,"ls":0,"fc":[1,1,1]},"t":0}]},"p":{},"m":{"g":1,"a":{"a":0,"k":[0,0],"ix":2}},"a":[{"nm":"Animator 1","s":{"t":0,"xe":{"a":0,"k":0,"ix":7},"ne":{"a":0,"k":0,"ix":8},"a":{"a":0,"k":100,"ix":4},"b":1,"rn":0,"sh":1,"sm":{"a":0,"k":100,"ix":6},"r":1},"a":{"p":{"a":1,"k":[{"i":{"x":0.25,"y":1},"o":{"x":0.33,"y":0},"t":30,"s":[0,50,0],"to":[0,0,0],"ti":[0,0,0]},{"t":45,"s":[0,0,0]}],"ix":2}}}]},"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":8,"ty":5,"nm":"Welcome to DeskThing 2","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[162.047,114,0],"ix":2,"l":2},"a":{"a":0,"k":[2.981,-22.559,0],"ix":1,"l":2},"s":{"a":0,"k":[134.638,134.638,100],"ix":6,"l":2}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"a","pt":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[3.503,-67.213],[3.237,125.676],[621.645,125.796],[621.91,-67.092]],"c":true},"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"Mask 1"}],"t":{"d":{"k":[{"s":{"s":94,"f":"Geist-Bold","t":"Welcome\u0003to DeskThing","ca":0,"j":0,"tr":0,"lh":94,"ls":0,"fc":[1,1,1]},"t":0}]},"p":{},"m":{"g":1,"a":{"a":0,"k":[0,0],"ix":2}},"a":[{"nm":"Animator 1","s":{"t":0,"xe":{"a":0,"k":0,"ix":7},"ne":{"a":0,"k":0,"ix":8},"a":{"a":0,"k":100,"ix":4},"b":1,"rn":0,"sh":1,"sm":{"a":0,"k":100,"ix":6},"r":1},"a":{"p":{"a":1,"k":[{"i":{"x":0.25,"y":1},"o":{"x":0.33,"y":0},"t":15,"s":[-606,0,0],"to":[0,0,0],"ti":[0,0,0]},{"t":45,"s":[0,0,0]}],"ix":2}}}]},"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":9,"ty":5,"nm":"Welcome to DeskThing","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[162.047,114,0],"ix":2,"l":2},"a":{"a":0,"k":[2.981,-22.559,0],"ix":1,"l":2},"s":{"a":0,"k":[134.638,134.638,100],"ix":6,"l":2}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"a","pt":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[3.503,-67.213],[3.237,125.676],[621.645,125.796],[621.91,-67.092]],"c":true},"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"Mask 1"}],"t":{"d":{"k":[{"s":{"s":94,"f":"Geist-Bold","t":"Welcome\u0003to DeskThing","ca":0,"j":0,"tr":0,"lh":94,"ls":0,"fc":[1,1,1]},"t":0}]},"p":{},"m":{"g":1,"a":{"a":0,"k":[0,0],"ix":2}},"a":[{"nm":"Animator 1","s":{"t":0,"xe":{"a":0,"k":0,"ix":7},"ne":{"a":0,"k":0,"ix":8},"a":{"a":0,"k":100,"ix":4},"b":1,"rn":0,"sh":1,"sm":{"a":0,"k":100,"ix":6},"r":1},"a":{"p":{"a":1,"k":[{"i":{"x":0.25,"y":1},"o":{"x":0.33,"y":0},"t":15,"s":[-606,0,0],"to":[0,0,0],"ti":[0,0,0]},{"t":45,"s":[0,0,0]}],"ix":2}}}]},"ip":0,"op":300,"st":0,"bm":0}],"markers":[],"chars":[{"ch":"W","size":94,"style":"Bold","w":100.5,"data":{"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[23.062,0],[39.377,0],[51.059,-45.721],[62.741,0],[79.257,0],[98.492,-71.503],[82.48,-71.503],[70.294,-21.149],[57.806,-71.503],[44.412,-71.503],[32.025,-21.149],[19.739,-71.503],[3.726,-71.503]],"c":true},"ix":2},"nm":"W","mn":"ADBE Vector Shape - Group","hd":false}],"nm":"W","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}]},"fFamily":"Geist"},{"ch":"e","size":94,"style":"Bold","w":59.5,"data":{"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-16.516,0],[-2.921,10.776],[0,0],[5.035,0],[0.403,8.56],[0,0],[0,0],[16.013,0],[0,-17.02]],"o":[[12.991,0],[0,0],[-1.41,4.431],[-6.949,0],[0,0],[0,0],[0,-18.127],[-16.415,0],[0,17.02]],"v":[[30.917,1.208],[56.497,-15.912],[41.29,-16.818],[31.219,-10.272],[19.336,-22.961],[57.303,-22.961],[57.303,-25.983],[30.817,-55.188],[3.726,-26.99]],"c":true},"ix":2},"nm":"e","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[-6.345,0],[-0.403,-7.855]],"o":[[0.806,-7.452],[6.244,0],[0,0]],"v":[[19.336,-32.227],[30.817,-44.009],[41.794,-32.227]],"c":true},"ix":2},"nm":"e","mn":"ADBE Vector Shape - Group","hd":false}],"nm":"e","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}]},"fFamily":"Geist"},{"ch":"l","size":94,"style":"Bold","w":30.3,"data":{"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-8.862,0],[0,0],[0,0],[0,0],[0,3.223],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[-2.82,0],[0,0],[0,0],[0,0],[0,9.97]],"v":[[20.746,0],[29.709,0],[29.709,-11.279],[26.083,-11.279],[21.753,-15.71],[21.753,-71.503],[6.647,-71.503],[6.647,-14.099]],"c":true},"ix":2},"nm":"l","mn":"ADBE Vector Shape - Group","hd":false}],"nm":"l","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}]},"fFamily":"Geist"},{"ch":"c","size":94,"style":"Bold","w":58.8,"data":{"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-16.617,0],[-1.511,13.293],[0,0],[5.841,0],[0,10.474],[-7.553,0],[-0.906,-6.647],[0,0],[14.401,0],[0,-17.02]],"o":[[14.703,0],[0,0],[-0.806,7.352],[-7.553,0],[0,-10.474],[5.74,0],[0,0],[-1.611,-13.293],[-16.617,0],[0,17.02]],"v":[[31.119,1.208],[57.504,-20.544],[41.895,-21.149],[31.119,-10.373],[19.235,-26.99],[31.119,-43.506],[41.895,-33.334],[57.504,-34.039],[31.119,-55.188],[3.726,-26.99]],"c":true},"ix":2},"nm":"c","mn":"ADBE Vector Shape - Group","hd":false}],"nm":"c","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}]},"fFamily":"Geist"},{"ch":"o","size":94,"style":"Bold","w":60.8,"data":{"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-16.516,0],[0,17.12],[16.516,0],[0,-17.02]],"o":[[16.516,0],[0,-17.02],[-16.516,0],[0,17.12]],"v":[[31.119,1.208],[58.511,-26.99],[31.119,-55.188],[3.726,-26.99]],"c":true},"ix":2},"nm":"o","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[7.553,0],[0,10.474],[-7.553,0],[0,-10.474]],"o":[[-7.553,0],[0,-10.474],[7.553,0],[0,10.474]],"v":[[31.119,-10.373],[19.235,-26.99],[31.119,-43.607],[43.002,-26.99]],"c":true},"ix":2},"nm":"o","mn":"ADBE Vector Shape - Group","hd":false}],"nm":"o","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}]},"fFamily":"Geist"},{"ch":"m","size":94,"style":"Bold","w":88.4,"data":{"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[-5.539,0],[0,-8.359],[0,0],[0,0],[0,0],[-5.539,0],[0,-8.459],[0,0],[0,0],[0,0],[11.078,0],[2.518,-7.15],[7.755,0],[2.719,-6.345],[0,0],[0,0]],"o":[[0,0],[0,0],[0,-8.157],[5.841,0],[0,0],[0,0],[0,0],[0,-8.157],[5.64,0],[0,0],[0,0],[0,0],[0,-13.293],[-7.15,0],[-2.316,-6.747],[-6.546,0],[0,0],[0,0],[0,0]],"v":[[6.647,0],[21.753,0],[21.753,-30.313],[30.414,-43.204],[38.47,-30.313],[38.47,0],[52.066,0],[52.066,-30.313],[60.727,-43.204],[68.884,-30.515],[68.884,0],[83.99,0],[83.99,-34.744],[65.863,-55.188],[50.656,-44.211],[35.248,-55.188],[20.544,-45.218],[20.242,-53.979],[6.647,-53.979]],"c":true},"ix":2},"nm":"m","mn":"ADBE Vector Shape - Group","hd":false}],"nm":"m","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}]},"fFamily":"Geist"},{"ch":"\u0003","size":94,"style":"Bold","w":0,"fFamily":"Geist"},{"ch":"t","size":94,"style":"Bold","w":43.6,"data":{"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-11.179,0],[0,0],[0,0],[0,0],[0,5.237],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[-4.431,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,11.279]],"v":[[29.004,0],[42.096,0],[42.096,-11.279],[34.543,-11.279],[27.896,-18.027],[27.896,-42.7],[42.096,-42.7],[42.096,-53.979],[27.896,-53.979],[27.896,-66.669],[12.79,-66.669],[12.79,-53.979],[4.129,-53.979],[4.129,-42.7],[12.79,-42.7],[12.79,-16.214]],"c":true},"ix":2},"nm":"t","mn":"ADBE Vector Shape - Group","hd":false}],"nm":"t","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}]},"fFamily":"Geist"},{"ch":" ","size":94,"style":"Bold","w":24.2,"data":{},"fFamily":"Geist"},{"ch":"D","size":94,"style":"Bold","w":70.4,"data":{"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,22.861],[23.163,0],[0,0]],"o":[[0,0],[22.76,0],[0,-22.961],[0,0],[0,0]],"v":[[7.452,0],[32.73,0],[68.28,-35.651],[32.126,-71.503],[7.452,-71.503]],"c":true},"ix":2},"nm":"D","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,-15.509],[13.797,0]],"o":[[0,0],[0,0],[13.797,0],[0,15.509],[0,0]],"v":[[22.76,-12.891],[22.76,-58.612],[32.126,-58.612],[52.469,-35.751],[32.126,-12.891]],"c":true},"ix":2},"nm":"D","mn":"ADBE Vector Shape - Group","hd":false}],"nm":"D","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}]},"fFamily":"Geist"},{"ch":"s","size":94,"style":"Bold","w":56.1,"data":{"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-16.718,0],[0,10.474],[17.322,2.518],[0,3.928],[-5.438,0],[-0.705,-5.237],[0,0],[15.207,0],[0,-10.373],[-17.12,-2.82],[0,-3.323],[5.438,0],[0.906,5.136],[0,0]],"o":[[14.703,0],[0,-9.366],[-7.452,-1.108],[0,-3.625],[5.338,0],[0,0],[-1.41,-10.776],[-15.912,0],[0,9.366],[9.265,1.511],[0,3.625],[-6.546,0],[0,0],[0.806,10.574]],"v":[[29.709,1.208],[53.879,-15.106],[30.615,-31.824],[20.544,-38.47],[28.903,-44.513],[38.269,-36.053],[53.476,-36.658],[29.004,-55.188],[5.035,-37.866],[27.292,-21.249],[38.269,-14.804],[29.709,-9.467],[19.034,-17.523],[3.726,-17.02]],"c":true},"ix":2},"nm":"s","mn":"ADBE Vector Shape - Group","hd":false}],"nm":"s","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}]},"fFamily":"Geist"},{"ch":"k","size":94,"style":"Bold","w":59.7,"data":{"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[6.647,0],[21.753,0],[21.753,-15.207],[29.205,-23.163],[44.009,0],[60.526,0],[38.974,-31.723],[59.921,-53.979],[41.794,-53.979],[21.753,-31.824],[21.753,-71.503],[6.647,-71.503]],"c":true},"ix":2},"nm":"k","mn":"ADBE Vector Shape - Group","hd":false}],"nm":"k","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}]},"fFamily":"Geist"},{"ch":"T","size":94,"style":"Bold","w":60.9,"data":{"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[22.458,0],[37.866,0],[37.866,-58.612],[59.317,-58.612],[59.317,-71.503],[1.108,-71.503],[1.108,-58.612],[22.458,-58.612]],"c":true},"ix":2},"nm":"T","mn":"ADBE Vector Shape - Group","hd":false}],"nm":"T","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}]},"fFamily":"Geist"},{"ch":"h","size":94,"style":"Bold","w":60.2,"data":{"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[-6.244,0],[0,-7.956],[0,0],[0,0],[0,0],[12.085,0],[2.921,-5.338],[0,0],[0,0]],"o":[[0,0],[0,0],[0,-8.157],[5.841,0],[0,0],[0,0],[0,0],[0,-11.984],[-6.445,0],[0,0],[0,0],[0,0]],"v":[[6.647,0],[21.753,0],[21.753,-30.716],[31.622,-43.204],[39.78,-31.119],[39.78,0],[54.886,0],[54.886,-34.744],[36.658,-55.188],[21.753,-47.333],[21.753,-71.503],[6.647,-71.503]],"c":true},"ix":2},"nm":"h","mn":"ADBE Vector Shape - Group","hd":false}],"nm":"h","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}]},"fFamily":"Geist"},{"ch":"i","size":94,"style":"Bold","w":26.7,"data":{"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[6.345,-60.626],[21.954,-60.626],[21.954,-72.711],[6.345,-72.711]],"c":true},"ix":2},"nm":"i","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[6.647,0],[21.753,0],[21.753,-53.979],[6.647,-53.979]],"c":true},"ix":2},"nm":"i","mn":"ADBE Vector Shape - Group","hd":false}],"nm":"i","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}]},"fFamily":"Geist"},{"ch":"n","size":94,"style":"Bold","w":59.5,"data":{"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[-6.244,0],[0,-8.661],[0,0],[0,0],[0,0],[11.581,0],[2.921,-7.352],[0,0],[0,0]],"o":[[0,0],[0,0],[0,-8.661],[6.345,0],[0,0],[0,0],[0,0],[0,-12.488],[-7.15,0],[0,0],[0,0],[0,0]],"v":[[6.647,0],[21.753,0],[21.753,-29.709],[31.522,-43.204],[39.78,-29.709],[39.78,0],[54.886,0],[54.886,-34.744],[36.557,-55.188],[20.544,-45.016],[20.242,-53.979],[6.647,-53.979]],"c":true},"ix":2},"nm":"n","mn":"ADBE Vector Shape - Group","hd":false}],"nm":"n","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}]},"fFamily":"Geist"},{"ch":"g","size":94,"style":"Bold","w":62.4,"data":{"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-15.408,0],[0,14.502],[0,0],[0,0],[0,0],[7.855,0],[0,-15.509],[-13.293,0],[-2.618,5.035],[0,0],[7.15,0],[1.108,3.525],[0,0]],"o":[[15.61,0],[0,0],[0,0],[0,0],[-2.82,-5.74],[-13.193,0],[0,16.013],[6.949,0],[0,0],[0,7.553],[-6.345,0],[0,0],[2.417,8.862]],"v":[[30.817,16.315],[57.202,-5.338],[57.202,-53.979],[42.398,-53.979],[42.398,-45.621],[26.184,-55.188],[3.726,-28.802],[26.385,-2.518],[41.995,-10.776],[41.995,-5.035],[30.817,5.74],[20.847,0.101],[5.338,1.208]],"c":true},"ix":2},"nm":"g","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[6.949,0],[0,9.265],[-7.251,0],[0.101,-9.467]],"o":[[-7.05,0],[0,-9.164],[7.05,0],[0,9.567]],"v":[[30.716,-13.696],[19.235,-28.903],[30.716,-44.11],[42.096,-28.903]],"c":true},"ix":2},"nm":"g","mn":"ADBE Vector Shape - Group","hd":false}],"nm":"g","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}]},"fFamily":"Geist"},{"ch":"V","size":40,"style":"Medium","w":60,"data":{"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[23.969,0],[36.456,0],[57.706,-71.503],[46.527,-71.503],[30.212,-14.099],[13.898,-71.503],[2.719,-71.503]],"c":true},"ix":2},"nm":"V","mn":"ADBE Vector Shape - Group","hd":false}],"nm":"V","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}]},"fFamily":"Geist Mono"},{"ch":"e","size":40,"style":"Medium","w":60,"data":{"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-15.106,0],[-3.122,10.172],[0,0],[6.345,0],[0.604,10.272],[0,0],[0,0],[14.2,0],[0,-16.919]],"o":[[11.581,0],[0,0],[-1.611,4.935],[-8.359,0],[0,0],[0,0],[0,-17.725],[-14.905,0],[0,17.02]],"v":[[30.917,1.208],[53.879,-15.61],[43.204,-16.315],[31.018,-8.157],[16.718,-23.767],[54.785,-23.767],[54.785,-26.587],[30.615,-54.785],[6.042,-26.788]],"c":true},"ix":2},"nm":"e","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[-7.553,0],[-0.806,-9.164]],"o":[[1.309,-8.862],[7.15,0],[0,0]],"v":[[16.718,-31.924],[30.615,-45.419],[43.808,-31.924]],"c":true},"ix":2},"nm":"e","mn":"ADBE Vector Shape - Group","hd":false}],"nm":"e","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}]},"fFamily":"Geist Mono"},{"ch":"r","size":40,"style":"Medium","w":60,"data":{"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[-7.755,0],[0,0],[0,0],[0,0],[1.813,-6.949],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,-8.459],[0,0],[0,0],[0,0],[-6.848,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[8.459,0],[48.944,0],[48.944,-8.56],[32.227,-8.56],[32.227,-31.824],[44.009,-44.714],[54.987,-44.714],[54.987,-53.577],[43.607,-53.577],[30.716,-43.304],[30.112,-53.577],[8.459,-53.577],[8.459,-45.016],[22.055,-45.016],[22.055,-8.56],[8.459,-8.56]],"c":true},"ix":2},"nm":"r","mn":"ADBE Vector Shape - Group","hd":false}],"nm":"r","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}]},"fFamily":"Geist Mono"},{"ch":"s","size":40,"style":"Medium","w":60,"data":{"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-14.401,0],[0,9.668],[16.214,2.82],[0,4.834],[-7.251,0],[-1.108,-5.136],[0,0],[14.2,0],[0,-9.366],[-15.811,-2.921],[0,-4.028],[7.251,0],[1.007,5.942],[0,0]],"o":[[12.991,0],[0,-8.862],[-9.668,-1.813],[0,-4.028],[6.747,0],[0,0],[-1.511,-9.265],[-13.696,0],[0,9.064],[10.675,1.913],[0,4.23],[-7.855,0],[0,0],[0.806,10.071]],"v":[[31.018,1.208],[52.872,-14.301],[32.227,-30.212],[19.336,-38.37],[29.709,-45.621],[41.693,-36.758],[52.066,-37.363],[29.709,-54.785],[8.862,-38.37],[29.608,-21.854],[42.499,-14.2],[31.119,-7.956],[17.926,-17.422],[7.553,-16.818]],"c":true},"ix":2},"nm":"s","mn":"ADBE Vector Shape - Group","hd":false}],"nm":"s","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}]},"fFamily":"Geist Mono"},{"ch":"i","size":40,"style":"Medium","w":60,"data":{"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[27.997,-61.331],[38.47,-61.331],[38.47,-71.906],[27.997,-71.906]],"c":true},"ix":2},"nm":"i","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[7.654,0],[56.799,0],[56.799,-8.56],[38.873,-8.56],[38.873,-53.577],[8.661,-53.577],[8.661,-45.016],[28.601,-45.016],[28.601,-8.56],[7.654,-8.56]],"c":true},"ix":2},"nm":"i","mn":"ADBE Vector Shape - Group","hd":false}],"nm":"i","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}]},"fFamily":"Geist Mono"},{"ch":"o","size":40,"style":"Medium","w":60,"data":{"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-15.005,0],[0,17.12],[14.905,0],[0,-17.12]],"o":[[14.905,0],[0,-17.12],[-15.005,0],[0,17.12]],"v":[[30.212,1.208],[54.987,-26.788],[30.212,-54.785],[5.438,-26.788]],"c":true},"ix":2},"nm":"o","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[8.963,0],[0,11.682],[-9.064,0],[0,-11.682]],"o":[[-9.064,0],[0,-11.682],[8.963,0],[0,11.682]],"v":[[30.212,-8.157],[16.113,-26.788],[30.212,-45.419],[44.312,-26.788]],"c":true},"ix":2},"nm":"o","mn":"ADBE Vector Shape - Group","hd":false}],"nm":"o","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}]},"fFamily":"Geist Mono"},{"ch":"n","size":40,"style":"Medium","w":60,"data":{"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[-7.452,0],[0,-9.265],[0,0],[0,0],[0,0],[11.984,0],[2.82,-7.452],[0,0],[0,0]],"o":[[0,0],[0,0],[0,-9.265],[7.15,0],[0,0],[0,0],[0,0],[0,-11.481],[-7.452,0],[0,0],[0,0],[0,0]],"v":[[8.057,0],[18.228,0],[18.228,-31.723],[31.522,-45.923],[42.197,-31.924],[42.197,0],[52.368,0],[52.368,-34.543],[34.442,-54.785],[17.624,-43.909],[17.422,-53.577],[8.057,-53.577]],"c":true},"ix":2},"nm":"n","mn":"ADBE Vector Shape - Group","hd":false}],"nm":"n","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}]},"fFamily":"Geist Mono"},{"ch":" ","size":40,"style":"Medium","w":60,"data":{},"fFamily":"Geist Mono"},{"ch":"v","size":40,"style":"Medium","w":60,"data":{"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[23.868,0],[36.557,0],[55.893,-53.577],[44.916,-53.577],[30.212,-10.776],[15.509,-53.577],[4.532,-53.577]],"c":true},"ix":2},"nm":"v","mn":"ADBE Vector Shape - Group","hd":false}],"nm":"v","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}]},"fFamily":"Geist Mono"},{"ch":"0","size":40,"style":"Medium","w":60,"data":{"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-15.811,0],[0,22.961],[15.811,0],[0,-23.062]],"o":[[15.811,0],[0,-23.062],[-15.811,0],[0,22.961]],"v":[[30.212,1.611],[55.591,-35.651],[30.212,-73.114],[4.834,-35.651]],"c":true},"ix":2},"nm":"0","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,5.942],[-8.963,0],[-2.216,-2.719],[0,0]],"o":[[0,-17.422],[3.323,0],[0,0],[-1.309,-4.129]],"v":[[15.811,-35.651],[30.212,-63.245],[38.672,-59.116],[17.725,-20.343]],"c":true},"ix":2},"nm":"0","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[8.963,0],[2.316,2.719],[0,0],[0,-6.042]],"o":[[-3.323,0],[0,0],[1.309,4.129],[0,17.221]],"v":[[30.212,-8.258],[21.652,-12.387],[42.7,-51.059],[44.614,-35.651]],"c":true},"ix":2},"nm":"0","mn":"ADBE Vector Shape - Group","hd":false}],"nm":"0","np":6,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}]},"fFamily":"Geist Mono"},{"ch":".","size":40,"style":"Medium","w":60,"data":{"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[23.364,0],[37.061,0],[37.061,-13.293],[23.364,-13.293]],"c":true},"ix":2},"nm":".","mn":"ADBE Vector Shape - Group","hd":false}],"nm":".","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}]},"fFamily":"Geist Mono"},{"ch":"9","size":40,"style":"Medium","w":60,"data":{"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[16.516,0],[0,-15.811],[-14.2,0],[-3.424,4.431],[9.668,0],[1.41,4.935],[0,0],[-13.193,0],[0,23.666]],"o":[[-15.106,0],[0,14.603],[7.452,0],[-1.41,15.509],[-7.05,0],[0,0],[2.719,10.071],[21.048,0],[0,-20.544]],"v":[[30.011,-73.114],[4.633,-47.534],[27.997,-23.364],[44.714,-31.018],[27.292,-8.258],[15.61,-16.516],[4.733,-15.61],[27.292,1.611],[55.792,-41.089]],"c":true},"ix":2},"nm":"9","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[-8.762,0],[0,-8.862],[8.963,0],[0,9.064]],"o":[[8.963,0],[0,9.064],[-8.359,0],[0,-9.769]],"v":[[30.011,-63.144],[44.614,-47.736],[29.205,-32.932],[15.61,-47.736]],"c":true},"ix":2},"nm":"9","mn":"ADBE Vector Shape - Group","hd":false}],"nm":"9","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}]},"fFamily":"Geist Mono"}]} \ No newline at end of file +{"v":"5.9.0","fr":30,"ip":0,"op":60,"w":1230,"h":390,"nm":"LandingAnimation_Centered","ddd":0,"assets":[],"fonts":{"list":[{"fName":"Geist-Bold","fFamily":"Geist","fStyle":"Bold","ascent":70.9991455078125},{"fName":"GeistMono-Medium","fFamily":"Geist Mono","fStyle":"Medium","ascent":70.9991455078125}]},"layers":[{"ddd":0,"ind":1,"ty":3,"nm":"NULL 1","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":10,"s":[679.5,146.941,0],"to":[0,0,0],"ti":[0,0,0]},{"t":45,"s":[310.393,146.941,0]}],"ix":2,"l":2},"a":{"a":0,"k":[50,50,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":3,"nm":"NULL","parent":1,"sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.5],"y":[1]},"o":{"x":[0.33],"y":[0]},"t":0,"s":[0]},{"t":45,"s":[-360]}],"ix":10},"p":{"a":0,"k":[-65.299,89.341,0],"ix":2,"l":2},"a":{"a":0,"k":[50,50,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"ip":0,"op":150,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"InsideMask","parent":2,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50,50.002,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-51.694,0],[0,51.694],[51.694,0],[0,-51.694]],"o":[[51.694,0],[0,-51.694],[-51.694,0],[0,51.694]],"v":[[0,93.6],[93.6,0],[0,-93.6],[-93.6,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.117647059262,0.843137264252,0.376470595598,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":40,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.5],"y":[1]},"o":{"x":[0.33],"y":[0]},"t":0,"s":[0]},{"t":35,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 2","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":150,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Inside","parent":2,"tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[49.025,49.837,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[242,242,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-23.57],[-23.79,0],[0,23.57],[23.79,0]],"o":[[0,23.57],[23.79,0],[0,-23.57],[-23.79,0]],"v":[[-43.15,0],[0,42.75],[43.15,0],[0,-42.75]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,20.12],[-20.34,0],[0,-20.12],[20.34,0]],"o":[[0,-20.12],[20.34,0],[0,20.12],[-20.34,0]],"v":[[-36.89,0],[0,-36.49],[36.89,0],[0,36.49]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0.133333340287,0.772549033165,0.368627458811,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":150,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"OutsideMask","parent":2,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50,49.998,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-41.36,-133.505],[-31.87,-173.275],[33.27,-173.275],[41.13,-133.005],[65.5,-123.555],[99.81,-145.935],[145.97,-98.215],[123.8,-63.315],[133.25,-40.025],[174.08,-32.005],[174.08,33.405],[133.25,41.925],[123.58,65.195],[146.42,100.095],[99.63,145.815],[64.71,123.385],[41.23,132.975],[32.23,173.285],[-33.15,173.285],[-41.6,132.195],[-65.92,123.285],[-100.17,145.545],[-146.41,99.695],[-124.24,64.555],[-133.95,41.725],[-173.84,32.745],[-174.08,-32.045],[-133.95,-40.025],[-124.1,-64.105],[-146.73,-98.215],[-99.5,-145.935],[-64.2,-124.055]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":25,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.5],"y":[1]},"o":{"x":[0.33],"y":[0]},"t":0,"s":[0]},{"t":35,"s":[100]}],"ix":2},"o":{"a":0,"k":4,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 2","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":150,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Outside","parent":2,"tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[49.025,49.817,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[242,242,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-3.33,0],[0,0],[0,0],[-0.68,3.26],[0,0],[0,0],[0,0],[-2.36,2.36],[0,0],[1.83,2.79],[0,0],[0,0],[0,0],[0,3.33],[0,0],[3.26,0.68],[0,0],[0,0],[0,0],[2.36,2.36],[0,0],[2.79,-1.83],[0,0],[0,0],[0,0],[3.33,0],[0,0],[0.68,-3.26],[0,0],[0,0],[0,0],[2.36,-2.36],[0,0],[-1.83,-2.79],[0,0],[0,0],[0,0],[0,-3.33],[0,0],[-3.26,-0.68],[0,0],[0,0],[0,0],[-2.36,-2.36],[0,0],[-2.79,1.83],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[3.34,0],[0,0],[0,0],[0,0],[2.79,1.82],[0,0],[2.36,-2.36],[0,0],[0,0],[0,0],[3.26,-0.68],[0,0],[0,-3.34],[0,0],[0,0],[0,0],[1.82,-2.79],[0,0],[-2.35,-2.36],[0,0],[0,0],[0,0],[-0.68,-3.26],[0,0],[-3.34,0],[0,0],[0,0],[0,0],[-2.79,-1.82],[0,0],[-2.36,2.35],[0,0],[0,0],[0,0],[-3.26,0.68],[0,0],[0,3.34],[0,0],[0,0],[0,0],[-1.82,2.79],[0,0],[2.36,2.36],[0,0],[0,0],[0,0],[0.68,3.26]],"v":[[-11.25,76.14],[11.25,76.14],[11.25,76.12],[18.18,70.49],[20.75,58.17],[26.47,55.8],[37,62.69],[45.88,61.77],[61.79,45.86],[62.71,36.98],[55.82,26.46],[58.19,20.74],[70.51,18.17],[76.14,11.24],[76.14,-11.26],[70.51,-18.19],[58.19,-20.76],[55.82,-26.48],[62.71,-37],[61.79,-45.88],[45.88,-61.79],[37,-62.71],[26.47,-55.82],[20.75,-58.19],[18.18,-70.51],[11.25,-76.14],[-11.25,-76.14],[-18.18,-70.51],[-20.75,-58.19],[-26.47,-55.82],[-37,-62.71],[-45.88,-61.79],[-61.79,-45.88],[-62.71,-37],[-55.82,-26.47],[-58.19,-20.75],[-70.51,-18.18],[-76.14,-11.25],[-76.14,11.25],[-70.51,18.18],[-58.19,20.75],[-55.82,26.47],[-62.71,37],[-61.79,45.88],[-45.88,61.79],[-37,62.71],[-26.47,55.82],[-20.75,58.19],[-18.18,70.51]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[2.16,0.9],[0,0],[1.96,-1.28],[0,0],[0,0],[0,0],[0.9,2.17],[0,0],[2.29,0.48],[0,0],[0,0],[0,0],[-0.9,2.17],[0,0],[1.28,1.96],[0,0],[0,0],[0,0],[-2.17,0.9],[0,0],[-0.48,2.29],[0,0],[0,0],[0,0],[-2.17,-0.9],[0,0],[-1.96,1.28],[0,0],[0,0],[0,0],[-0.9,-2.17],[0,0],[-2.29,-0.48],[0,0],[0,0],[0,0],[0.9,-2.16],[0,0],[-1.28,-1.96],[0,0],[0,0],[0,0],[2.17,-0.9],[0,0],[0.48,-2.29]],"o":[[0,0],[0,0],[0,0],[-0.48,-2.29],[0,0],[-2.17,-0.89],[0,0],[0,0],[0,0],[1.29,-1.96],[0,0],[-0.89,-2.16],[0,0],[0,0],[0,0],[2.3,-0.48],[0,0],[0.9,-2.17],[0,0],[0,0],[0,0],[1.96,1.28],[0,0],[2.16,-0.89],[0,0],[0,0],[0,0],[0.48,2.3],[0,0],[2.17,0.9],[0,0],[0,0],[0,0],[-1.29,1.96],[0,0],[0.89,2.17],[0,0],[0,0],[0,0],[-2.29,0.48],[0,0],[-0.9,2.17],[0,0],[0,0],[0,0],[-1.96,-1.29],[0,0],[-2.17,0.89],[0,0]],"v":[[10.22,67.8],[-10.22,67.8],[-10.21,67.79],[-12.7,55.82],[-16.92,50.73],[-23.88,47.85],[-30.47,48.47],[-40.69,55.17],[-55.14,40.71],[-48.44,30.49],[-47.82,23.9],[-50.7,16.95],[-55.79,12.73],[-67.76,10.24],[-67.76,-10.2],[-55.8,-12.69],[-50.7,-16.91],[-47.82,-23.86],[-48.44,-30.45],[-55.14,-40.67],[-40.69,-55.13],[-30.47,-48.43],[-23.88,-47.81],[-16.92,-50.69],[-12.7,-55.78],[-10.21,-67.74],[10.23,-67.74],[12.72,-55.78],[16.94,-50.68],[23.89,-47.8],[30.48,-48.42],[40.7,-55.12],[55.15,-40.66],[48.45,-30.44],[47.83,-23.85],[50.71,-16.9],[55.8,-12.68],[67.77,-10.19],[67.77,10.25],[55.8,12.74],[50.71,16.96],[47.83,23.92],[48.45,30.51],[55.15,40.73],[40.69,55.18],[30.47,48.48],[23.88,47.86],[16.93,50.74],[12.71,55.83]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":150,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":5,"nm":"Version v0.9","cl":"9","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[165.298,-14,0],"ix":2,"l":2},"a":{"a":0,"k":[2.981,-22.559,0],"ix":1,"l":2},"s":{"a":0,"k":[134.638,134.638,100],"ix":6,"l":2}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"a","pt":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[1.088,-29.246],[1.088,0.645],[390.702,0.599],[390.702,-29.292]],"c":true},"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"Mask 1"}],"t":{"d":{"k":[{"s":{"s":40,"f":"GeistMono-Medium","t":"Version v0.9","ca":0,"j":0,"tr":0,"lh":94,"ls":0,"fc":[1,1,1]},"t":0}]},"p":{},"m":{"g":1,"a":{"a":0,"k":[0,0],"ix":2}},"a":[{"nm":"Animator 1","s":{"t":0,"xe":{"a":0,"k":0,"ix":7},"ne":{"a":0,"k":0,"ix":8},"a":{"a":0,"k":100,"ix":4},"b":1,"rn":0,"sh":1,"sm":{"a":0,"k":100,"ix":6},"r":1},"a":{"p":{"a":1,"k":[{"i":{"x":0.25,"y":1},"o":{"x":0.33,"y":0},"t":30,"s":[0,50,0],"to":[0,0,0],"ti":[0,0,0]},{"t":45,"s":[0,0,0]}],"ix":2}}}]},"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":8,"ty":5,"nm":"Welcome to DeskThing 2","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[162.047,114,0],"ix":2,"l":2},"a":{"a":0,"k":[2.981,-22.559,0],"ix":1,"l":2},"s":{"a":0,"k":[134.638,134.638,100],"ix":6,"l":2}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"a","pt":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[3.503,-67.213],[3.237,125.676],[621.645,125.796],[621.91,-67.092]],"c":true},"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"Mask 1"}],"t":{"d":{"k":[{"s":{"s":94,"f":"Geist-Bold","t":"Welcome\u0003to DeskThing","ca":0,"j":0,"tr":0,"lh":94,"ls":0,"fc":[1,1,1]},"t":0}]},"p":{},"m":{"g":1,"a":{"a":0,"k":[0,0],"ix":2}},"a":[{"nm":"Animator 1","s":{"t":0,"xe":{"a":0,"k":0,"ix":7},"ne":{"a":0,"k":0,"ix":8},"a":{"a":0,"k":100,"ix":4},"b":1,"rn":0,"sh":1,"sm":{"a":0,"k":100,"ix":6},"r":1},"a":{"p":{"a":1,"k":[{"i":{"x":0.25,"y":1},"o":{"x":0.33,"y":0},"t":15,"s":[-606,0,0],"to":[0,0,0],"ti":[0,0,0]},{"t":45,"s":[0,0,0]}],"ix":2}}}]},"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":9,"ty":5,"nm":"Welcome to DeskThing","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[162.047,114,0],"ix":2,"l":2},"a":{"a":0,"k":[2.981,-22.559,0],"ix":1,"l":2},"s":{"a":0,"k":[134.638,134.638,100],"ix":6,"l":2}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"a","pt":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[3.503,-67.213],[3.237,125.676],[621.645,125.796],[621.91,-67.092]],"c":true},"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"Mask 1"}],"t":{"d":{"k":[{"s":{"s":94,"f":"Geist-Bold","t":"Welcome\u0003to DeskThing","ca":0,"j":0,"tr":0,"lh":94,"ls":0,"fc":[1,1,1]},"t":0}]},"p":{},"m":{"g":1,"a":{"a":0,"k":[0,0],"ix":2}},"a":[{"nm":"Animator 1","s":{"t":0,"xe":{"a":0,"k":0,"ix":7},"ne":{"a":0,"k":0,"ix":8},"a":{"a":0,"k":100,"ix":4},"b":1,"rn":0,"sh":1,"sm":{"a":0,"k":100,"ix":6},"r":1},"a":{"p":{"a":1,"k":[{"i":{"x":0.25,"y":1},"o":{"x":0.33,"y":0},"t":15,"s":[-606,0,0],"to":[0,0,0],"ti":[0,0,0]},{"t":45,"s":[0,0,0]}],"ix":2}}}]},"ip":0,"op":300,"st":0,"bm":0}],"markers":[],"chars":[{"ch":"W","size":94,"style":"Bold","w":100.5,"data":{"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[23.062,0],[39.377,0],[51.059,-45.721],[62.741,0],[79.257,0],[98.492,-71.503],[82.48,-71.503],[70.294,-21.149],[57.806,-71.503],[44.412,-71.503],[32.025,-21.149],[19.739,-71.503],[3.726,-71.503]],"c":true},"ix":2},"nm":"W","mn":"ADBE Vector Shape - Group","hd":false}],"nm":"W","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}]},"fFamily":"Geist"},{"ch":"e","size":94,"style":"Bold","w":59.5,"data":{"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-16.516,0],[-2.921,10.776],[0,0],[5.035,0],[0.403,8.56],[0,0],[0,0],[16.013,0],[0,-17.02]],"o":[[12.991,0],[0,0],[-1.41,4.431],[-6.949,0],[0,0],[0,0],[0,-18.127],[-16.415,0],[0,17.02]],"v":[[30.917,1.208],[56.497,-15.912],[41.29,-16.818],[31.219,-10.272],[19.336,-22.961],[57.303,-22.961],[57.303,-25.983],[30.817,-55.188],[3.726,-26.99]],"c":true},"ix":2},"nm":"e","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[-6.345,0],[-0.403,-7.855]],"o":[[0.806,-7.452],[6.244,0],[0,0]],"v":[[19.336,-32.227],[30.817,-44.009],[41.794,-32.227]],"c":true},"ix":2},"nm":"e","mn":"ADBE Vector Shape - Group","hd":false}],"nm":"e","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}]},"fFamily":"Geist"},{"ch":"l","size":94,"style":"Bold","w":30.3,"data":{"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-8.862,0],[0,0],[0,0],[0,0],[0,3.223],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[-2.82,0],[0,0],[0,0],[0,0],[0,9.97]],"v":[[20.746,0],[29.709,0],[29.709,-11.279],[26.083,-11.279],[21.753,-15.71],[21.753,-71.503],[6.647,-71.503],[6.647,-14.099]],"c":true},"ix":2},"nm":"l","mn":"ADBE Vector Shape - Group","hd":false}],"nm":"l","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}]},"fFamily":"Geist"},{"ch":"c","size":94,"style":"Bold","w":58.8,"data":{"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-16.617,0],[-1.511,13.293],[0,0],[5.841,0],[0,10.474],[-7.553,0],[-0.906,-6.647],[0,0],[14.401,0],[0,-17.02]],"o":[[14.703,0],[0,0],[-0.806,7.352],[-7.553,0],[0,-10.474],[5.74,0],[0,0],[-1.611,-13.293],[-16.617,0],[0,17.02]],"v":[[31.119,1.208],[57.504,-20.544],[41.895,-21.149],[31.119,-10.373],[19.235,-26.99],[31.119,-43.506],[41.895,-33.334],[57.504,-34.039],[31.119,-55.188],[3.726,-26.99]],"c":true},"ix":2},"nm":"c","mn":"ADBE Vector Shape - Group","hd":false}],"nm":"c","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}]},"fFamily":"Geist"},{"ch":"o","size":94,"style":"Bold","w":60.8,"data":{"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-16.516,0],[0,17.12],[16.516,0],[0,-17.02]],"o":[[16.516,0],[0,-17.02],[-16.516,0],[0,17.12]],"v":[[31.119,1.208],[58.511,-26.99],[31.119,-55.188],[3.726,-26.99]],"c":true},"ix":2},"nm":"o","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[7.553,0],[0,10.474],[-7.553,0],[0,-10.474]],"o":[[-7.553,0],[0,-10.474],[7.553,0],[0,10.474]],"v":[[31.119,-10.373],[19.235,-26.99],[31.119,-43.607],[43.002,-26.99]],"c":true},"ix":2},"nm":"o","mn":"ADBE Vector Shape - Group","hd":false}],"nm":"o","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}]},"fFamily":"Geist"},{"ch":"m","size":94,"style":"Bold","w":88.4,"data":{"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[-5.539,0],[0,-8.359],[0,0],[0,0],[0,0],[-5.539,0],[0,-8.459],[0,0],[0,0],[0,0],[11.078,0],[2.518,-7.15],[7.755,0],[2.719,-6.345],[0,0],[0,0]],"o":[[0,0],[0,0],[0,-8.157],[5.841,0],[0,0],[0,0],[0,0],[0,-8.157],[5.64,0],[0,0],[0,0],[0,0],[0,-13.293],[-7.15,0],[-2.316,-6.747],[-6.546,0],[0,0],[0,0],[0,0]],"v":[[6.647,0],[21.753,0],[21.753,-30.313],[30.414,-43.204],[38.47,-30.313],[38.47,0],[52.066,0],[52.066,-30.313],[60.727,-43.204],[68.884,-30.515],[68.884,0],[83.99,0],[83.99,-34.744],[65.863,-55.188],[50.656,-44.211],[35.248,-55.188],[20.544,-45.218],[20.242,-53.979],[6.647,-53.979]],"c":true},"ix":2},"nm":"m","mn":"ADBE Vector Shape - Group","hd":false}],"nm":"m","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}]},"fFamily":"Geist"},{"ch":"\u0003","size":94,"style":"Bold","w":0,"fFamily":"Geist"},{"ch":"t","size":94,"style":"Bold","w":43.6,"data":{"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-11.179,0],[0,0],[0,0],[0,0],[0,5.237],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[-4.431,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,11.279]],"v":[[29.004,0],[42.096,0],[42.096,-11.279],[34.543,-11.279],[27.896,-18.027],[27.896,-42.7],[42.096,-42.7],[42.096,-53.979],[27.896,-53.979],[27.896,-66.669],[12.79,-66.669],[12.79,-53.979],[4.129,-53.979],[4.129,-42.7],[12.79,-42.7],[12.79,-16.214]],"c":true},"ix":2},"nm":"t","mn":"ADBE Vector Shape - Group","hd":false}],"nm":"t","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}]},"fFamily":"Geist"},{"ch":" ","size":94,"style":"Bold","w":24.2,"data":{},"fFamily":"Geist"},{"ch":"D","size":94,"style":"Bold","w":70.4,"data":{"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,22.861],[23.163,0],[0,0]],"o":[[0,0],[22.76,0],[0,-22.961],[0,0],[0,0]],"v":[[7.452,0],[32.73,0],[68.28,-35.651],[32.126,-71.503],[7.452,-71.503]],"c":true},"ix":2},"nm":"D","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,-15.509],[13.797,0]],"o":[[0,0],[0,0],[13.797,0],[0,15.509],[0,0]],"v":[[22.76,-12.891],[22.76,-58.612],[32.126,-58.612],[52.469,-35.751],[32.126,-12.891]],"c":true},"ix":2},"nm":"D","mn":"ADBE Vector Shape - Group","hd":false}],"nm":"D","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}]},"fFamily":"Geist"},{"ch":"s","size":94,"style":"Bold","w":56.1,"data":{"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-16.718,0],[0,10.474],[17.322,2.518],[0,3.928],[-5.438,0],[-0.705,-5.237],[0,0],[15.207,0],[0,-10.373],[-17.12,-2.82],[0,-3.323],[5.438,0],[0.906,5.136],[0,0]],"o":[[14.703,0],[0,-9.366],[-7.452,-1.108],[0,-3.625],[5.338,0],[0,0],[-1.41,-10.776],[-15.912,0],[0,9.366],[9.265,1.511],[0,3.625],[-6.546,0],[0,0],[0.806,10.574]],"v":[[29.709,1.208],[53.879,-15.106],[30.615,-31.824],[20.544,-38.47],[28.903,-44.513],[38.269,-36.053],[53.476,-36.658],[29.004,-55.188],[5.035,-37.866],[27.292,-21.249],[38.269,-14.804],[29.709,-9.467],[19.034,-17.523],[3.726,-17.02]],"c":true},"ix":2},"nm":"s","mn":"ADBE Vector Shape - Group","hd":false}],"nm":"s","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}]},"fFamily":"Geist"},{"ch":"k","size":94,"style":"Bold","w":59.7,"data":{"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[6.647,0],[21.753,0],[21.753,-15.207],[29.205,-23.163],[44.009,0],[60.526,0],[38.974,-31.723],[59.921,-53.979],[41.794,-53.979],[21.753,-31.824],[21.753,-71.503],[6.647,-71.503]],"c":true},"ix":2},"nm":"k","mn":"ADBE Vector Shape - Group","hd":false}],"nm":"k","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}]},"fFamily":"Geist"},{"ch":"T","size":94,"style":"Bold","w":60.9,"data":{"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[22.458,0],[37.866,0],[37.866,-58.612],[59.317,-58.612],[59.317,-71.503],[1.108,-71.503],[1.108,-58.612],[22.458,-58.612]],"c":true},"ix":2},"nm":"T","mn":"ADBE Vector Shape - Group","hd":false}],"nm":"T","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}]},"fFamily":"Geist"},{"ch":"h","size":94,"style":"Bold","w":60.2,"data":{"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[-6.244,0],[0,-7.956],[0,0],[0,0],[0,0],[12.085,0],[2.921,-5.338],[0,0],[0,0]],"o":[[0,0],[0,0],[0,-8.157],[5.841,0],[0,0],[0,0],[0,0],[0,-11.984],[-6.445,0],[0,0],[0,0],[0,0]],"v":[[6.647,0],[21.753,0],[21.753,-30.716],[31.622,-43.204],[39.78,-31.119],[39.78,0],[54.886,0],[54.886,-34.744],[36.658,-55.188],[21.753,-47.333],[21.753,-71.503],[6.647,-71.503]],"c":true},"ix":2},"nm":"h","mn":"ADBE Vector Shape - Group","hd":false}],"nm":"h","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}]},"fFamily":"Geist"},{"ch":"i","size":94,"style":"Bold","w":26.7,"data":{"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[6.345,-60.626],[21.954,-60.626],[21.954,-72.711],[6.345,-72.711]],"c":true},"ix":2},"nm":"i","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[6.647,0],[21.753,0],[21.753,-53.979],[6.647,-53.979]],"c":true},"ix":2},"nm":"i","mn":"ADBE Vector Shape - Group","hd":false}],"nm":"i","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}]},"fFamily":"Geist"},{"ch":"n","size":94,"style":"Bold","w":59.5,"data":{"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[-6.244,0],[0,-8.661],[0,0],[0,0],[0,0],[11.581,0],[2.921,-7.352],[0,0],[0,0]],"o":[[0,0],[0,0],[0,-8.661],[6.345,0],[0,0],[0,0],[0,0],[0,-12.488],[-7.15,0],[0,0],[0,0],[0,0]],"v":[[6.647,0],[21.753,0],[21.753,-29.709],[31.522,-43.204],[39.78,-29.709],[39.78,0],[54.886,0],[54.886,-34.744],[36.557,-55.188],[20.544,-45.016],[20.242,-53.979],[6.647,-53.979]],"c":true},"ix":2},"nm":"n","mn":"ADBE Vector Shape - Group","hd":false}],"nm":"n","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}]},"fFamily":"Geist"},{"ch":"g","size":94,"style":"Bold","w":62.4,"data":{"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-15.408,0],[0,14.502],[0,0],[0,0],[0,0],[7.855,0],[0,-15.509],[-13.293,0],[-2.618,5.035],[0,0],[7.15,0],[1.108,3.525],[0,0]],"o":[[15.61,0],[0,0],[0,0],[0,0],[-2.82,-5.74],[-13.193,0],[0,16.013],[6.949,0],[0,0],[0,7.553],[-6.345,0],[0,0],[2.417,8.862]],"v":[[30.817,16.315],[57.202,-5.338],[57.202,-53.979],[42.398,-53.979],[42.398,-45.621],[26.184,-55.188],[3.726,-28.802],[26.385,-2.518],[41.995,-10.776],[41.995,-5.035],[30.817,5.74],[20.847,0.101],[5.338,1.208]],"c":true},"ix":2},"nm":"g","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[6.949,0],[0,9.265],[-7.251,0],[0.101,-9.467]],"o":[[-7.05,0],[0,-9.164],[7.05,0],[0,9.567]],"v":[[30.716,-13.696],[19.235,-28.903],[30.716,-44.11],[42.096,-28.903]],"c":true},"ix":2},"nm":"g","mn":"ADBE Vector Shape - Group","hd":false}],"nm":"g","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}]},"fFamily":"Geist"},{"ch":"V","size":40,"style":"Medium","w":60,"data":{"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[23.969,0],[36.456,0],[57.706,-71.503],[46.527,-71.503],[30.212,-14.099],[13.898,-71.503],[2.719,-71.503]],"c":true},"ix":2},"nm":"V","mn":"ADBE Vector Shape - Group","hd":false}],"nm":"V","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}]},"fFamily":"Geist Mono"},{"ch":"e","size":40,"style":"Medium","w":60,"data":{"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-15.106,0],[-3.122,10.172],[0,0],[6.345,0],[0.604,10.272],[0,0],[0,0],[14.2,0],[0,-16.919]],"o":[[11.581,0],[0,0],[-1.611,4.935],[-8.359,0],[0,0],[0,0],[0,-17.725],[-14.905,0],[0,17.02]],"v":[[30.917,1.208],[53.879,-15.61],[43.204,-16.315],[31.018,-8.157],[16.718,-23.767],[54.785,-23.767],[54.785,-26.587],[30.615,-54.785],[6.042,-26.788]],"c":true},"ix":2},"nm":"e","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[-7.553,0],[-0.806,-9.164]],"o":[[1.309,-8.862],[7.15,0],[0,0]],"v":[[16.718,-31.924],[30.615,-45.419],[43.808,-31.924]],"c":true},"ix":2},"nm":"e","mn":"ADBE Vector Shape - Group","hd":false}],"nm":"e","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}]},"fFamily":"Geist Mono"},{"ch":"r","size":40,"style":"Medium","w":60,"data":{"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[-7.755,0],[0,0],[0,0],[0,0],[1.813,-6.949],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,-8.459],[0,0],[0,0],[0,0],[-6.848,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[8.459,0],[48.944,0],[48.944,-8.56],[32.227,-8.56],[32.227,-31.824],[44.009,-44.714],[54.987,-44.714],[54.987,-53.577],[43.607,-53.577],[30.716,-43.304],[30.112,-53.577],[8.459,-53.577],[8.459,-45.016],[22.055,-45.016],[22.055,-8.56],[8.459,-8.56]],"c":true},"ix":2},"nm":"r","mn":"ADBE Vector Shape - Group","hd":false}],"nm":"r","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}]},"fFamily":"Geist Mono"},{"ch":"s","size":40,"style":"Medium","w":60,"data":{"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-14.401,0],[0,9.668],[16.214,2.82],[0,4.834],[-7.251,0],[-1.108,-5.136],[0,0],[14.2,0],[0,-9.366],[-15.811,-2.921],[0,-4.028],[7.251,0],[1.007,5.942],[0,0]],"o":[[12.991,0],[0,-8.862],[-9.668,-1.813],[0,-4.028],[6.747,0],[0,0],[-1.511,-9.265],[-13.696,0],[0,9.064],[10.675,1.913],[0,4.23],[-7.855,0],[0,0],[0.806,10.071]],"v":[[31.018,1.208],[52.872,-14.301],[32.227,-30.212],[19.336,-38.37],[29.709,-45.621],[41.693,-36.758],[52.066,-37.363],[29.709,-54.785],[8.862,-38.37],[29.608,-21.854],[42.499,-14.2],[31.119,-7.956],[17.926,-17.422],[7.553,-16.818]],"c":true},"ix":2},"nm":"s","mn":"ADBE Vector Shape - Group","hd":false}],"nm":"s","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}]},"fFamily":"Geist Mono"},{"ch":"i","size":40,"style":"Medium","w":60,"data":{"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[27.997,-61.331],[38.47,-61.331],[38.47,-71.906],[27.997,-71.906]],"c":true},"ix":2},"nm":"i","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[7.654,0],[56.799,0],[56.799,-8.56],[38.873,-8.56],[38.873,-53.577],[8.661,-53.577],[8.661,-45.016],[28.601,-45.016],[28.601,-8.56],[7.654,-8.56]],"c":true},"ix":2},"nm":"i","mn":"ADBE Vector Shape - Group","hd":false}],"nm":"i","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}]},"fFamily":"Geist Mono"},{"ch":"o","size":40,"style":"Medium","w":60,"data":{"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-15.005,0],[0,17.12],[14.905,0],[0,-17.12]],"o":[[14.905,0],[0,-17.12],[-15.005,0],[0,17.12]],"v":[[30.212,1.208],[54.987,-26.788],[30.212,-54.785],[5.438,-26.788]],"c":true},"ix":2},"nm":"o","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[8.963,0],[0,11.682],[-9.064,0],[0,-11.682]],"o":[[-9.064,0],[0,-11.682],[8.963,0],[0,11.682]],"v":[[30.212,-8.157],[16.113,-26.788],[30.212,-45.419],[44.312,-26.788]],"c":true},"ix":2},"nm":"o","mn":"ADBE Vector Shape - Group","hd":false}],"nm":"o","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}]},"fFamily":"Geist Mono"},{"ch":"n","size":40,"style":"Medium","w":60,"data":{"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[-7.452,0],[0,-9.265],[0,0],[0,0],[0,0],[11.984,0],[2.82,-7.452],[0,0],[0,0]],"o":[[0,0],[0,0],[0,-9.265],[7.15,0],[0,0],[0,0],[0,0],[0,-11.481],[-7.452,0],[0,0],[0,0],[0,0]],"v":[[8.057,0],[18.228,0],[18.228,-31.723],[31.522,-45.923],[42.197,-31.924],[42.197,0],[52.368,0],[52.368,-34.543],[34.442,-54.785],[17.624,-43.909],[17.422,-53.577],[8.057,-53.577]],"c":true},"ix":2},"nm":"n","mn":"ADBE Vector Shape - Group","hd":false}],"nm":"n","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}]},"fFamily":"Geist Mono"},{"ch":" ","size":40,"style":"Medium","w":60,"data":{},"fFamily":"Geist Mono"},{"ch":"v","size":40,"style":"Medium","w":60,"data":{"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[23.868,0],[36.557,0],[55.893,-53.577],[44.916,-53.577],[30.212,-10.776],[15.509,-53.577],[4.532,-53.577]],"c":true},"ix":2},"nm":"v","mn":"ADBE Vector Shape - Group","hd":false}],"nm":"v","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}]},"fFamily":"Geist Mono"},{"ch":"0","size":40,"style":"Medium","w":60,"data":{"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-15.811,0],[0,22.961],[15.811,0],[0,-23.062]],"o":[[15.811,0],[0,-23.062],[-15.811,0],[0,22.961]],"v":[[30.212,1.611],[55.591,-35.651],[30.212,-73.114],[4.834,-35.651]],"c":true},"ix":2},"nm":"0","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,5.942],[-8.963,0],[-2.216,-2.719],[0,0]],"o":[[0,-17.422],[3.323,0],[0,0],[-1.309,-4.129]],"v":[[15.811,-35.651],[30.212,-63.245],[38.672,-59.116],[17.725,-20.343]],"c":true},"ix":2},"nm":"0","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[8.963,0],[2.316,2.719],[0,0],[0,-6.042]],"o":[[-3.323,0],[0,0],[1.309,4.129],[0,17.221]],"v":[[30.212,-8.258],[21.652,-12.387],[42.7,-51.059],[44.614,-35.651]],"c":true},"ix":2},"nm":"0","mn":"ADBE Vector Shape - Group","hd":false}],"nm":"0","np":6,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}]},"fFamily":"Geist Mono"},{"ch":".","size":40,"style":"Medium","w":60,"data":{"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[23.364,0],[37.061,0],[37.061,-13.293],[23.364,-13.293]],"c":true},"ix":2},"nm":".","mn":"ADBE Vector Shape - Group","hd":false}],"nm":".","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}]},"fFamily":"Geist Mono"},{"ch":"9","size":40,"style":"Medium","w":60,"data":{"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[16.516,0],[0,-15.811],[-14.2,0],[-3.424,4.431],[9.668,0],[1.41,4.935],[0,0],[-13.193,0],[0,23.666]],"o":[[-15.106,0],[0,14.603],[7.452,0],[-1.41,15.509],[-7.05,0],[0,0],[2.719,10.071],[21.048,0],[0,-20.544]],"v":[[30.011,-73.114],[4.633,-47.534],[27.997,-23.364],[44.714,-31.018],[27.292,-8.258],[15.61,-16.516],[4.733,-15.61],[27.292,1.611],[55.792,-41.089]],"c":true},"ix":2},"nm":"9","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[-8.762,0],[0,-8.862],[8.963,0],[0,9.064]],"o":[[8.963,0],[0,9.064],[-8.359,0],[0,-9.769]],"v":[[30.011,-63.144],[44.614,-47.736],[29.205,-32.932],[15.61,-47.736]],"c":true},"ix":2},"nm":"9","mn":"ADBE Vector Shape - Group","hd":false}],"nm":"9","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}]},"fFamily":"Geist Mono"}]} \ No newline at end of file diff --git a/DeskThingServer/src/renderer/src/assets/icons/icon/IconToggle.tsx b/DeskThingServer/src/renderer/src/assets/icons/icon/IconToggle.tsx index 6ef3df5c..afcc11b0 100644 --- a/DeskThingServer/src/renderer/src/assets/icons/icon/IconToggle.tsx +++ b/DeskThingServer/src/renderer/src/assets/icons/icon/IconToggle.tsx @@ -3,9 +3,10 @@ import { useEffect, useRef } from 'react' interface ToggleProps extends IconProps { checked: boolean + disabled?: boolean } -function IconToggle({ checked, ...props }: ToggleProps): JSX.Element { +function IconToggle({ disabled = false, checked, ...props }: ToggleProps): JSX.Element { const circleRef = useRef(null) useEffect(() => { @@ -29,9 +30,16 @@ function IconToggle({ checked, ...props }: ToggleProps): JSX.Element { stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" + style={{ opacity: disabled ? 0.5 : 1, cursor: disabled ? 'not-allowed' : 'pointer' }} > - + ADB -> Restart Server)', weight: 4, minimum: 32 }, { message: 'Have you tried asking the device nicely?', weight: 1, minimum: 19 }, @@ -55,87 +55,87 @@ export const deviceMessages = [ { message: "Plot twist: The device was working all along, and we're in a simulation!", weight: 1, - minimum: 50 + minimum: 12 }, { message: 'Why do we try if it only continues to bring suffering', weight: 1, - minimum: 50 + minimum: 12 }, { message: 'I aspire to have your level of persistance', weight: 1, - minimum: 50 + minimum: 15 }, { message: 'I dont think you understand how difficult it is to debug this', weight: 1, - minimum: 50 + minimum: 15 }, { message: 'Okay I get it - I dont think its working', weight: 1, - minimum: 50 + minimum: 20 }, { message: "You've been at this for quite awhile now, I think you should take a break", weight: 1, - minimum: 50 + minimum: 20 }, { message: 'TOUCH GRASS', weight: 1, - minimum: 70 + minimum: 30 }, { message: '"Insanity is doing the same thing over and over again and expecting different results,"', weight: 1, - minimum: 70 + minimum: 30 }, { message: 'Try one more time. I believe in you', weight: 1, - minimum: 70 + minimum: 30 }, { message: 'Im sure the problem is something simple, you just need to find it', weight: 1, - minimum: 70 + minimum: 30 }, { message: 'At this point just ask on the discord!', weight: 1, - minimum: 120 + minimum: 50 }, { message: 'At this point just ask on the discord!', weight: 1, - minimum: 120 + minimum: 50 }, { message: 'At this point just ask on the discord!', weight: 1, - minimum: 120 + minimum: 50 }, { message: 'Some people never give up!', weight: 1, - minimum: 150 + minimum: 60 }, { message: 'Woah! You have a lot of patience. This means youve been trying for a while', weight: 1, - minimum: 1000 + minimum: 60 }, { message: 'This is an easter egg. Send a screenshot of this over the discord', weight: 1, - minimum: 1200 + minimum: 100 }, { message: 'There are no more messages. You have reached the end of the line', weight: 1, - minimum: 1250 + minimum: 110 } ] diff --git a/DeskThingServer/src/renderer/src/components/Connection.tsx b/DeskThingServer/src/renderer/src/components/Connection.tsx index 7ce26ad6..0111b273 100644 --- a/DeskThingServer/src/renderer/src/components/Connection.tsx +++ b/DeskThingServer/src/renderer/src/components/Connection.tsx @@ -30,6 +30,7 @@ const ConnectionComponent: React.FC = ({ client }) => const [showLogging, setShowLogging] = useState(false) const [offline, setOffline] = useState(false) const refreshADbClients = useClientStore((store) => store.requestADBDevices) + const requestClientManifest = useClientStore((store) => store.requestClientManifest) const devicePort = useSettingsStore((store) => store.settings.devicePort) useEffect(() => { @@ -106,9 +107,7 @@ const ConnectionComponent: React.FC = ({ client }) => setLogging(reply) if (reply.final) { unsubscribe() - } - if (!reply.status) { - unsubscribe() + requestClientManifest() } }) } catch (error) { @@ -222,12 +221,13 @@ const ConnectionComponent: React.FC = ({ client }) => {!client.connected && ( diff --git a/DeskThingServer/src/renderer/src/components/NotificationButton.tsx b/DeskThingServer/src/renderer/src/components/NotificationButton.tsx index 70a3764d..a4267d0a 100644 --- a/DeskThingServer/src/renderer/src/components/NotificationButton.tsx +++ b/DeskThingServer/src/renderer/src/components/NotificationButton.tsx @@ -9,6 +9,7 @@ const NotificationButton: React.FC = () => { const taskNum = useNotificationStore((state) => state.totalTasks) const logs = useNotificationStore((state) => state.logs) + const requests = useNotificationStore((state) => state.requestQueue) const issues = useNotificationStore((state) => state.issues.length) const [errors, setErrors] = useState(0) @@ -30,8 +31,11 @@ const NotificationButton: React.FC = () => { + + + + ))} + + ) +} + +export default RankableList diff --git a/DeskThingServer/src/renderer/src/components/Select.tsx b/DeskThingServer/src/renderer/src/components/Select.tsx new file mode 100644 index 00000000..a8b4e181 --- /dev/null +++ b/DeskThingServer/src/renderer/src/components/Select.tsx @@ -0,0 +1,102 @@ +import { SettingOption } from '@shared/types' +import React from 'react' +import ReactSelect, { MultiValue, SingleValue } from 'react-select' + +interface SelectProps { + options: SettingOption[] + value: string[] | string + isMulti?: boolean + placeholder: string + className?: string + onChange: (value: SingleValue | MultiValue) => void +} +const Select: React.FC = ({ + options, + isMulti, + onChange, + value, + placeholder, + className +}) => { + // We can't use tailwind css here because of how classes are passed into the child components + const customStyles = { + control: (provided) => ({ + ...provided, + backgroundColor: 'rgb(24, 24, 27)', // Tailwind `bg-zinc-900` + borderColor: 'rgb(63, 63, 70)', // Tailwind `border-zinc-700` + color: 'white', + padding: '0.25rem 0.5rem', // Tailwind `p-2` + boxShadow: 'none', + '&:hover': { + borderColor: 'rgb(113, 113, 122)' // Tailwind `border-zinc-600` + } + }), + menu: (provided) => ({ + ...provided, + backgroundColor: 'rgb(24, 24, 27)', // Tailwind `bg-zinc-900` + color: 'white', + borderRadius: '0.25rem', // Tailwind `rounded-md` + padding: '0.5rem' // Tailwind `p-2` + }), + menuList: (provided) => ({ + ...provided, + padding: 0 + }), + option: (provided, state) => ({ + ...provided, + backgroundColor: state.isSelected + ? 'rgb(34, 197, 94)' // Tailwind `bg-green-500` + : state.isFocused + ? 'rgb(39, 39, 42)' // Tailwind `bg-zinc-800` + : 'transparent', + color: state.isSelected ? 'white' : 'rgb(229, 231, 235)', // Tailwind `text-gray-200` + padding: '0.5rem 1rem', // Tailwind `px-4 py-2` + '&:hover': { + backgroundColor: 'rgb(39, 39, 42)', // Tailwind `bg-zinc-800` + color: 'rgb(229, 231, 235)' // Tailwind `text-gray-200` + } + }), + singleValue: (provided) => ({ + ...provided, + color: 'white' + }), + multiValue: (provided) => ({ + ...provided, + backgroundColor: 'rgb(63, 63, 70)', // Tailwind `bg-zinc-700` + color: 'white' + }), + multiValueLabel: (provided) => ({ + ...provided, + color: 'rgb(229, 231, 235)' // Tailwind `text-gray-200` + }), + multiValueRemove: (provided) => ({ + ...provided, + color: 'rgb(229, 231, 235)', // Tailwind `text-gray-200` + '&:hover': { + backgroundColor: 'rgb(185, 28, 28)', // Tailwind `bg-red-700` + color: 'white' + } + }) + } + + return ( + <> + value.includes(option.value)) + : options.find((option) => option.value === value) + } + options={options} + className={className} + isMulti={isMulti} + closeMenuOnSelect={!isMulti} + styles={customStyles} + placeholder={placeholder} + onChange={onChange} + /> + + ) +} + +export default Select diff --git a/DeskThingServer/src/renderer/src/components/settings/SettingsString.tsx b/DeskThingServer/src/renderer/src/components/settings/SettingsString.tsx new file mode 100644 index 00000000..e69de29b diff --git a/DeskThingServer/src/renderer/src/listeners/LogDataListener.tsx b/DeskThingServer/src/renderer/src/listeners/LogDataListener.tsx index 0fed88a1..3c926152 100644 --- a/DeskThingServer/src/renderer/src/listeners/LogDataListener.tsx +++ b/DeskThingServer/src/renderer/src/listeners/LogDataListener.tsx @@ -1,47 +1,23 @@ import { useEffect } from 'react' import { useLogStore, useNotificationStore } from '../stores' +import { Log } from '@shared/types' const LogDataListener = (): null => { const addLog = useLogStore((state) => state.addLog) const addLogs = useNotificationStore((state) => state.addLog) useEffect(() => { - const handleError = (_event, errorData): void => { - if (typeof errorData != 'string') { - console.log(errorData) - errorData = JSON.stringify(errorData) - } - addLog('error', errorData) - addLogs('error', errorData) - } - - const handleLog = (_event, logData): void => { - if (typeof logData != 'string') { - console.log(logData) - logData = JSON.stringify(logData) - } - addLog('log', logData) - } - - const handleMessage = (_event, messageData): void => { - if (typeof messageData != 'string') { - console.log(messageData) - messageData = JSON.stringify(messageData) - } - addLog('message', messageData) - addLogs('log', messageData) + const handleLog = (_event, log: Log): void => { + addLog(log) + addLogs(log) } // Listen for IPC events - window.electron.ipcRenderer.on('error', handleError) window.electron.ipcRenderer.on('log', handleLog) - window.electron.ipcRenderer.on('message', handleMessage) // Clean up the IPC listeners when the component unmounts return () => { - window.electron.ipcRenderer.removeAllListeners('error') window.electron.ipcRenderer.removeAllListeners('log') - window.electron.ipcRenderer.removeAllListeners('message') } }, [addLog]) diff --git a/DeskThingServer/src/renderer/src/nav/Nav.tsx b/DeskThingServer/src/renderer/src/nav/Nav.tsx index 09d019fe..d5a7a6a0 100644 --- a/DeskThingServer/src/renderer/src/nav/Nav.tsx +++ b/DeskThingServer/src/renderer/src/nav/Nav.tsx @@ -65,7 +65,7 @@ const Nav: React.FC = () => { location="Developer" currentPage={currentPage} handleNavigation={handleNavigation} - subDirectories={['App', 'ADB', 'Logs']} + subDirectories={['Logs', 'App', 'ADB']} > Dev diff --git a/DeskThingServer/src/renderer/src/overlays/Overlay.tsx b/DeskThingServer/src/renderer/src/overlays/Overlay.tsx index 6e957cc5..d09d0e8d 100644 --- a/DeskThingServer/src/renderer/src/overlays/Overlay.tsx +++ b/DeskThingServer/src/renderer/src/overlays/Overlay.tsx @@ -25,7 +25,7 @@ const Overlay: React.FC = ({ onClose, className, chil }, [onClose]) return ( -
+
+
+ + ) +} diff --git a/DeskThingServer/src/renderer/src/overlays/apps/AppDetails.tsx b/DeskThingServer/src/renderer/src/overlays/apps/AppDetails.tsx index 90a472c2..7f13a61d 100644 --- a/DeskThingServer/src/renderer/src/overlays/apps/AppDetails.tsx +++ b/DeskThingServer/src/renderer/src/overlays/apps/AppDetails.tsx @@ -51,7 +51,7 @@ const AppDetails: React.FC = ({ app }: AppSettingProps) => { rel="noreferrer noopener" className="text-blue-500 hover:text-blue-400" > - {app.manifest.homepage} + {app.manifest.repository}
diff --git a/DeskThingServer/src/renderer/src/overlays/apps/AppSettings.tsx b/DeskThingServer/src/renderer/src/overlays/apps/AppSettings.tsx index 672a9867..d393ce8d 100644 --- a/DeskThingServer/src/renderer/src/overlays/apps/AppSettings.tsx +++ b/DeskThingServer/src/renderer/src/overlays/apps/AppSettings.tsx @@ -1,9 +1,12 @@ import React, { useEffect, useState, useCallback, useMemo } from 'react' import { useAppStore } from '@renderer/stores' -import { AppDataInterface, SettingsType } from '@shared/types' +import { AppDataInterface, SettingOption, SettingsString, SettingsType } from '@shared/types' import { AppSettingProps } from './AppsOverlay' import Button from '@renderer/components/Button' -import { IconCheck, IconLoading, IconSave, IconToggle, IconX } from '@renderer/assets/icons' +import { IconLoading, IconSave, IconToggle } from '@renderer/assets/icons' +import Select from '@renderer/components/Select' +import { MultiValue, SingleValue } from 'react-select' +import RankableList from '@renderer/components/RankableList' const AppSettings: React.FC = ({ app }) => { const getAppData = useAppStore((state) => state.getAppData) @@ -42,10 +45,16 @@ const AppSettings: React.FC = ({ app }) => { [] ) + const clampValue = (value: number, min?: number, max?: number): number => { + if (min !== undefined && value < min) return min + if (max !== undefined && value > max) return max + return value + } + const renderSettingInput = useCallback( (setting: SettingsType, key: string) => { const commonClasses = - 'mt-1 block px-3 py-2 bg-black border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm' + 'mt-1 block px-3 py-2 bg-zinc-900 border border-zinc-700 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm' switch (setting.type || '') { case 'string': @@ -55,6 +64,7 @@ const AppSettings: React.FC = ({ app }) => { handleSettingChange(key, e.target.value)} className={commonClasses + ' w-full'} /> @@ -71,7 +81,11 @@ const AppSettings: React.FC = ({ app }) => { value={setting.value as number} min={setting.min} max={setting.max} - onChange={(e) => handleSettingChange(key, Number(e.target.value))} + onChange={(e) => { + let inputValue = Number(e.target.value) + inputValue = clampValue(inputValue, setting.min, setting.max) + handleSettingChange(key, inputValue) + }} className={commonClasses} /> )} @@ -90,21 +104,37 @@ const AppSettings: React.FC = ({ app }) => { ) - case 'select': + case 'range': return ( - {setting.type == 'select' && ( - + className="w-96 max-w-s" + /> + )} + + ) + case 'select': + return ( + + {setting.type == 'select' && ( +
+ { + const selectedValues = selected as MultiValue + const currentValues = selectedValues.map((value) => value.value) + handleSettingChange(key, currentValues) + }} + /> +
+ )} +
+ ) + case 'ranked': + return ( + + {setting.type == 'ranked' && ( +
+ { + handleSettingChange(key, rankedValues) + }} + />
)}
@@ -203,12 +241,17 @@ const SettingComponent = ({ setting, children, className }: SettingComponentProp
-
-

- {setting.type?.toUpperCase() || 'Legacy Setting'} -

-
-

{setting.label}

+
+
+

{setting.type?.toUpperCase() || 'Legacy Setting'}

+ {setting.type === 'number' && ( +

+ MIN: {setting.min} | MAX: {setting.max} +

+ )} +
+
+

{setting.label}

{setting.description && (
{setting.description} @@ -216,7 +259,10 @@ const SettingComponent = ({ setting, children, className }: SettingComponentProp )}
- {children} +
+ {setting.type === 'range' &&
{setting.value}
} + {children} +
) } diff --git a/DeskThingServer/src/renderer/src/overlays/notifications/EventsPage.tsx b/DeskThingServer/src/renderer/src/overlays/notifications/EventsPage.tsx index 93756901..0c049114 100644 --- a/DeskThingServer/src/renderer/src/overlays/notifications/EventsPage.tsx +++ b/DeskThingServer/src/renderer/src/overlays/notifications/EventsPage.tsx @@ -2,6 +2,7 @@ import React from 'react' import { useNotificationStore } from '@renderer/stores' import { IconTrash } from '@renderer/assets/icons' import Button from '@renderer/components/Button' +import { MESSAGE_TYPES } from '@shared/types' const EvensPage: React.FC = () => { const logs = useNotificationStore((state) => state.logs) @@ -21,21 +22,27 @@ const EvensPage: React.FC = () => {
  • ))} diff --git a/DeskThingServer/src/renderer/src/overlays/notifications/NotificationOverlay.tsx b/DeskThingServer/src/renderer/src/overlays/notifications/NotificationOverlay.tsx index 3fe835cf..15aa3ebc 100644 --- a/DeskThingServer/src/renderer/src/overlays/notifications/NotificationOverlay.tsx +++ b/DeskThingServer/src/renderer/src/overlays/notifications/NotificationOverlay.tsx @@ -60,7 +60,12 @@ const NotificationOverlay: React.FC = () => { curPage={page} value={notifState.requestQueue.length} Icon={} - /> + className="relative" + > + {notifState.requestQueue.length > 0 && ( +
    + )} + ( ) diff --git a/DeskThingServer/src/renderer/src/overlays/notifications/RequestsPage.tsx b/DeskThingServer/src/renderer/src/overlays/notifications/RequestsPage.tsx index 1fa94c6e..a01fd665 100644 --- a/DeskThingServer/src/renderer/src/overlays/notifications/RequestsPage.tsx +++ b/DeskThingServer/src/renderer/src/overlays/notifications/RequestsPage.tsx @@ -43,7 +43,7 @@ interface RequestProps { const RequestComponent = ({ request }: RequestProps): React.ReactElement => { const resolveRequest = useNotificationStore((state) => state.resolveRequest) const [expanded, setIsExpanded] = useState(false) - const [focusedIndex, setFocusedIndex] = useState(-1) + const [focusedIndex, setFocusedIndex] = useState(0) const [formData, setFormData] = useState<{ [key: string]: string }>({}) const [allFieldsFilled, setAllFieldsFilled] = useState(false) @@ -73,7 +73,7 @@ const RequestComponent = ({ request }: RequestProps): React.ReactElement => { const toggleExpanded = (): void => { setIsExpanded(!expanded) - setFocusedIndex(-1) + setFocusedIndex(0) } useEffect(() => { diff --git a/DeskThingServer/src/renderer/src/overlays/settings/ClientSettings.tsx b/DeskThingServer/src/renderer/src/overlays/settings/ClientSettings.tsx index f0068414..c4b5da86 100644 --- a/DeskThingServer/src/renderer/src/overlays/settings/ClientSettings.tsx +++ b/DeskThingServer/src/renderer/src/overlays/settings/ClientSettings.tsx @@ -1,5 +1,5 @@ import React, { useState } from 'react' -import { useClientStore, useAppStore } from '@renderer/stores' +import { useClientStore } from '@renderer/stores' import { ClientManifest } from '@shared/types' import Button from '@renderer/components/Button' import { IconToggle, IconSave, IconLoading } from '@renderer/assets/icons' @@ -7,7 +7,6 @@ import { IconToggle, IconSave, IconLoading } from '@renderer/assets/icons' const ClientSettings: React.FC = () => { const clientSettings = useClientStore((state) => state.clientManifest) const updateClientSettings = useClientStore((state) => state.updateClientManifest) - const apps = useAppStore((state) => state.appsList) const [localSettings, setLocalSettings] = useState(clientSettings) const [loading, setLoading] = useState(false) @@ -34,34 +33,6 @@ const ClientSettings: React.FC = () => { return (
    -
    -

    Default View

    - -
    -
    -

    Miniplayer Mode

    - -

    IP Address

    diff --git a/DeskThingServer/src/renderer/src/overlays/settings/MusicSettings.tsx b/DeskThingServer/src/renderer/src/overlays/settings/MusicSettings.tsx index 9b70bf3d..699c9988 100644 --- a/DeskThingServer/src/renderer/src/overlays/settings/MusicSettings.tsx +++ b/DeskThingServer/src/renderer/src/overlays/settings/MusicSettings.tsx @@ -3,13 +3,16 @@ import useSettingsStore from '../../stores/settingsStore' import useAppStore from '../../stores/appStore' import Button from '@renderer/components/Button' import { IconLoading, IconSave, IconToggle } from '@renderer/assets/icons' +import Select from '@renderer/components/Select' +import { SingleValue } from 'react-select' +import { SettingOption, Settings } from '@shared/types' const MusicSettings: React.FC = () => { - const initialSettings = useSettingsStore((settings) => settings.settings) const saveSettings = useSettingsStore((settings) => settings.saveSettings) + const requestSettings = useSettingsStore((settings) => settings.requestSettings) const appsList = useAppStore((state) => state.appsList) const [audioSources, setAudioSources] = useState<{ id: string; name: string }[]>([]) - const [settings, setSettings] = useState(initialSettings) + const [settings, setSettings] = useState(null) const [loading, setLoading] = useState(false) useEffect(() => { @@ -20,14 +23,23 @@ const MusicSettings: React.FC = () => { name: app.manifest?.label || app.name })) setAudioSources(sources) + + const fetchSettings = async (): Promise => { + const settings = await requestSettings() + setSettings(settings) + } + + fetchSettings() }, [appsList]) const handleSettingChange = (key: string, value: string | boolean | number | string[]): void => { + if (!settings) return setSettings({ ...settings, [key]: value }) console.log('Settings Updated:', settings) } const handleSave = async (): Promise => { + if (!settings) return setLoading(true) await saveSettings(settings) setTimeout(() => { @@ -43,55 +55,68 @@ const MusicSettings: React.FC = () => {

    Refresh Interval (seconds)

    handleSettingChange('refreshInterval', Number(e.target.value) * 1000) } className="focus:text-white bg-zinc-900 text-white rounded px-2 py-2" placeholder="Enter A Value" - disabled={settings.refreshInterval === -1} + disabled={!settings || settings.refreshInterval === -1} />
    -

    Playback Sources

    - { - handleSettingChange('playbackLocation', e.target.value) + const value = e as SingleValue + handleSettingChange('playbackLocation', value?.value || '') }} - defaultValue={'Unset'} - className="bg-zinc-900 rounded hover:cursor-pointer text-white px-2 py-2" - > - {audioSources.map((app) => ( - - ))} - - + value={settings ? settings.playbackLocation || '' : 'Disabled'} + className="bg-zinc-900 rounded hover:cursor-pointer text-white px-2 py-2 w-full" + options={[ + ...audioSources.map((app) => ({ + value: app.id, + label: app.name + })), + { + value: 'none', + label: 'None' + }, + { + value: 'disabled', + label: 'Disabled' + } + ]} + />
    +
    +

    Logging Level

    +