diff --git a/package-lock.json b/package-lock.json index ec39923..32cce83 100644 --- a/package-lock.json +++ b/package-lock.json @@ -735,6 +735,88 @@ } } }, + "@angular/pwa": { + "version": "0.1001.7", + "resolved": "https://registry.npmjs.org/@angular/pwa/-/pwa-0.1001.7.tgz", + "integrity": "sha512-/vGJ/Z6lY8qT3fT1DzJ4D2iz0WYVxJSnmXAhXwltxJQwKHkgJZFnEzOudPpIVMVs9LO68KLnoM6wshU4ZyFgkg==", + "requires": { + "@angular-devkit/core": "10.1.7", + "@angular-devkit/schematics": "10.1.7", + "@schematics/angular": "10.1.7", + "parse5-html-rewriting-stream": "6.0.1" + }, + "dependencies": { + "@angular-devkit/core": { + "version": "10.1.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-10.1.7.tgz", + "integrity": "sha512-RRyDkN2FByA+nlnRx/MzUMK1FXwj7+SsrzJcvZfWx4yA5rfKmJiJryXQEzL44GL1aoaXSuvOYu3H72wxZADN8Q==", + "requires": { + "ajv": "6.12.4", + "fast-json-stable-stringify": "2.1.0", + "magic-string": "0.25.7", + "rxjs": "6.6.2", + "source-map": "0.7.3" + } + }, + "@angular-devkit/schematics": { + "version": "10.1.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-10.1.7.tgz", + "integrity": "sha512-nk9RXA09b+7uq59HS/gyztNzUGHH/eQAUQhWHdDYSCG6v1lhJVCKx1HgDPELVxmeq9f+HArkAW7Y7c+ccdNQ7A==", + "requires": { + "@angular-devkit/core": "10.1.7", + "ora": "5.0.0", + "rxjs": "6.6.2" + } + }, + "@schematics/angular": { + "version": "10.1.7", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-10.1.7.tgz", + "integrity": "sha512-jcyLWDSbpgHvB/BNVSsV4uLJpC2qRx9Z5+rcQpBB1BerqIPS/1cTQg7TViHZtcqnZqWvzHR3jfqzDUSOCZpuJQ==", + "requires": { + "@angular-devkit/core": "10.1.7", + "@angular-devkit/schematics": "10.1.7", + "jsonc-parser": "2.3.0" + } + }, + "ajv": { + "version": "6.12.4", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.4.tgz", + "integrity": "sha512-eienB2c9qVQs2KWexhkrdMLVDoIQCz5KSeLxwg9Lzk4DOfBtIK9PQwwufcsn1jjGuf9WZmqPMbGxOzfcuphJCQ==", + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, + "magic-string": { + "version": "0.25.7", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", + "integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==", + "requires": { + "sourcemap-codec": "^1.4.4" + } + }, + "rxjs": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.2.tgz", + "integrity": "sha512-BHdBMVoWC2sL26w//BCu3YzKT4s2jip/WhwsGEDmeKYBhKDZeYezVUnHatYB7L85v5xs0BAQmg6BEYJEKxBabg==", + "requires": { + "tslib": "^1.9.0" + } + } + } + }, "@angular/router": { "version": "10.1.5", "resolved": "https://registry.npmjs.org/@angular/router/-/router-10.1.5.tgz", @@ -3624,7 +3706,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", - "dev": true, "requires": { "restore-cursor": "^3.1.0" } @@ -3632,8 +3713,7 @@ "cli-spinners": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.5.0.tgz", - "integrity": "sha512-PC+AmIuK04E6aeSs/pUccSujsTzBhu4HzC2dL+CfJB/Jcc2qTRbEwZQDfIUpt2Xl8BodYBEq8w4fc0kU2I9DjQ==", - "dev": true + "integrity": "sha512-PC+AmIuK04E6aeSs/pUccSujsTzBhu4HzC2dL+CfJB/Jcc2qTRbEwZQDfIUpt2Xl8BodYBEq8w4fc0kU2I9DjQ==" }, "cli-width": { "version": "3.0.0", @@ -4651,7 +4731,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=", - "dev": true, "requires": { "clone": "^1.0.2" }, @@ -4659,8 +4738,7 @@ "clone": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=", - "dev": true + "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=" } } }, @@ -4813,6 +4891,11 @@ "integrity": "sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==", "dev": true }, + "dexie": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/dexie/-/dexie-3.0.2.tgz", + "integrity": "sha512-go4FnIoAhcUiCdxutfIZRxnSaSyDgfEq+GH7N0I8nTCJbC2FmeBj+0FrETa3ln5ix+VQMOPsFeYHlgE/8SZWwQ==" + }, "dezalgo": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.3.tgz", @@ -7038,8 +7121,7 @@ "is-interactive": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", - "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", - "dev": true + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==" }, "is-negative-zero": { "version": "2.0.0", @@ -7493,8 +7575,7 @@ "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, "json-stringify-safe": { "version": "5.0.1", @@ -7520,8 +7601,7 @@ "jsonc-parser": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-2.3.0.tgz", - "integrity": "sha512-b0EBt8SWFNnixVdvoR2ZtEGa9ZqLhbJnOjezn+WP+8kspFm+PFYDN8Z4Bc7pRlDjvuVcADSUkroIuTWWn/YiIA==", - "dev": true + "integrity": "sha512-b0EBt8SWFNnixVdvoR2ZtEGa9ZqLhbJnOjezn+WP+8kspFm+PFYDN8Z4Bc7pRlDjvuVcADSUkroIuTWWn/YiIA==" }, "jsonfile": { "version": "4.0.0", @@ -8055,7 +8135,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.0.0.tgz", "integrity": "sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA==", - "dev": true, "requires": { "chalk": "^4.0.0" }, @@ -8064,7 +8143,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "requires": { "color-convert": "^2.0.1" } @@ -8073,7 +8151,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", - "dev": true, "requires": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -8083,7 +8160,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "requires": { "color-name": "~1.1.4" } @@ -8091,20 +8167,17 @@ "color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, "requires": { "has-flag": "^4.0.0" } @@ -8412,8 +8485,7 @@ "mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" }, "mini-css-extract-plugin": { "version": "0.10.0", @@ -8653,8 +8725,7 @@ "mute-stream": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", - "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", - "dev": true + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==" }, "nanomatch": { "version": "1.2.13", @@ -8700,6 +8771,14 @@ "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=", "dev": true }, + "ng-connection-service": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/ng-connection-service/-/ng-connection-service-1.0.4.tgz", + "integrity": "sha512-WrZfK+hUzrJS77ItxXI08rUN6Av77W3+LsaJEPufyo2wRe7Tn8xG18FHHEbbgqKkJeDT/yGJBH2xOaT+1jb22g==", + "requires": { + "tslib": "^1.9.0" + } + }, "ngx-markdown": { "version": "10.1.1", "resolved": "https://registry.npmjs.org/ngx-markdown/-/ngx-markdown-10.1.1.tgz", @@ -9239,7 +9318,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, "requires": { "mimic-fn": "^2.1.0" } @@ -9293,7 +9371,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/ora/-/ora-5.0.0.tgz", "integrity": "sha512-s26qdWqke2kjN/wC4dy+IQPBIMWBJlSU/0JZhk30ZDBLelW25rv66yutUWARMigpGPzcXHb+Nac5pNhN/WsARw==", - "dev": true, "requires": { "chalk": "^4.1.0", "cli-cursor": "^3.1.0", @@ -9308,14 +9385,12 @@ "ansi-regex": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", - "dev": true + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "requires": { "color-convert": "^2.0.1" } @@ -9324,7 +9399,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", - "dev": true, "requires": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -9334,7 +9408,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "requires": { "color-name": "~1.1.4" } @@ -9342,20 +9415,17 @@ "color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" }, "strip-ansi": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, "requires": { "ansi-regex": "^5.0.0" } @@ -9364,7 +9434,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, "requires": { "has-flag": "^4.0.0" } @@ -9693,8 +9762,16 @@ "parse5": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", - "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", - "dev": true + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==" + }, + "parse5-html-rewriting-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5-html-rewriting-stream/-/parse5-html-rewriting-stream-6.0.1.tgz", + "integrity": "sha512-vwLQzynJVEfUlURxgnf51yAJDQTtVpNyGD8tKi2Za7m+akukNHxCcUQMAa/mUGLhCeicFdpy7Tlvj8ZNKadprg==", + "requires": { + "parse5": "^6.0.1", + "parse5-sax-parser": "^6.0.1" + } }, "parse5-htmlparser2-tree-adapter": { "version": "6.0.1", @@ -9705,6 +9782,14 @@ "parse5": "^6.0.1" } }, + "parse5-sax-parser": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5-sax-parser/-/parse5-sax-parser-6.0.1.tgz", + "integrity": "sha512-kXX+5S81lgESA0LsDuGjAlBybImAChYRMT+/uKCEXFBFOeEhS52qUCydGhU3qLRD8D9DVjaUo821WK7DM4iCeg==", + "requires": { + "parse5": "^6.0.1" + } + }, "parseqs": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz", @@ -10968,8 +11053,7 @@ "punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" }, "q": { "version": "1.4.1", @@ -11438,7 +11522,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", - "dev": true, "requires": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" @@ -11940,8 +12023,7 @@ "signal-exit": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", - "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", - "dev": true + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==" }, "simple-swizzle": { "version": "0.2.2", @@ -12316,8 +12398,7 @@ "source-map": { "version": "0.7.3", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", - "dev": true + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==" }, "source-map-loader": { "version": "1.0.2", @@ -12389,8 +12470,7 @@ "sourcemap-codec": { "version": "1.4.8", "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", - "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", - "dev": true + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==" }, "spdx-correct": { "version": "3.1.1", @@ -13432,7 +13512,6 @@ "version": "4.4.0", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.0.tgz", "integrity": "sha512-B0yRTzYdUCCn9n+F4+Gh4yIDtMQcaJsmYBDsTSG8g/OejKBodLQ2IHfN3bM7jUsRXndopT7OIXWdYqc1fjmV6g==", - "dev": true, "requires": { "punycode": "^2.1.0" } @@ -13886,7 +13965,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", "integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=", - "dev": true, "requires": { "defaults": "^1.0.3" } diff --git a/package.json b/package.json index 32df540..a10f167 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,9 @@ "ag-grid-angular": "^22.1.1", "ag-grid-community": "^22.1.1", "core-js": "^2.5.4", + "dexie": "^3.0.2", "moment": "^2.24.0", + "ng-connection-service": "^1.0.4", "ngx-markdown": "^10.1.1", "ngx-monaco-editor": "^8.1.1", "rxjs": "~6.6.3", diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 4ff2b6d..0648889 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -21,6 +21,7 @@ import {SessionService} from './service/session.service'; import {SearchComponent} from './components/search/Search.component'; import {NodeTypeIcons} from './structures/node-types'; import {NavigationService} from './service/navigation.service'; +import {DatabaseService} from './service/db/database.service'; @Component({ selector: 'app-root', @@ -79,6 +80,7 @@ export class AppComponent implements OnInit { protected hasSearchOpen = false; protected versionInterval?: any; protected showedNewVersionAlert = false; + protected showedOfflineAlert = false; protected initialized$: BehaviorSubject = new BehaviorSubject(false); constructor( @@ -94,11 +96,14 @@ export class AppComponent implements OnInit { protected loading: LoadingController, protected navService: NavigationService, protected toasts: ToastController, + protected db: DatabaseService, ) { this.initializeApp(); } - _doInit() { + async _doInit() { + await this.db.createSchemata(); + this.reloadMenuItems().subscribe(() => { this.ready$.next(true); setTimeout(() => { @@ -402,17 +407,16 @@ export class AppComponent implements OnInit { reloadMenuItems() { return new Observable(sub => { - this.api.get('/menu/items').subscribe(result => { - this.nodes = result.data; - setTimeout(() => { - sub.next(); - sub.complete(); - }, 0); + this.api.getMenuItems().then(nodes => { + this.nodes = nodes; + sub.next(); + sub.complete(); }); }); } async initializeApp() { + console.log('app', this); this.loader = await this.loading.create({ message: 'Starting up...', cssClass: 'noded-loading-mask', @@ -420,8 +424,24 @@ export class AppComponent implements OnInit { }); await this.loader.present(); - await this.platform.ready(); + + let toast: any; + this.api.offline$.subscribe(async isOffline => { + if ( isOffline && !this.showedOfflineAlert ) { + toast = await this.toasts.create({ + cssClass: 'compat-toast-container', + message: 'Uh, oh! It looks like you\'re offline. Some features might not work as expected...', + }); + + this.showedOfflineAlert = true; + await toast.present(); + } else if ( !isOffline && this.showedOfflineAlert ) { + await toast.dismiss(); + this.showedOfflineAlert = false; + } + }); + const stat: any = await this.session.stat(); if ( !stat.authenticated_user ) { diff --git a/src/app/app.module.ts b/src/app/app.module.ts index e7629ad..976d2a3 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -15,6 +15,7 @@ import {AgGridModule} from 'ag-grid-angular'; import {MonacoEditorModule} from 'ngx-monaco-editor'; import { APP_BASE_HREF, PlatformLocation } from '@angular/common'; import { MarkdownModule } from 'ngx-markdown'; +import {ConnectionServiceModule} from 'ng-connection-service'; /** * This function is used internal to get a string instance of the `` value from `index.html`. @@ -44,6 +45,7 @@ export function getBaseHref(platformLocation: PlatformLocation): string { AgGridModule.withComponents([]), MonacoEditorModule.forRoot(), MarkdownModule.forRoot(), + ConnectionServiceModule, ], providers: [ StatusBar, diff --git a/src/app/service/api.service.ts b/src/app/service/api.service.ts index c99af7d..78f2ca6 100644 --- a/src/app/service/api.service.ts +++ b/src/app/service/api.service.ts @@ -1,8 +1,11 @@ -import { Injectable } from '@angular/core'; -import { environment } from '../../environments/environment'; +import {Injectable} from '@angular/core'; +import {environment} from '../../environments/environment'; import {HttpClient} from '@angular/common/http'; -import {Observable} from 'rxjs'; +import {BehaviorSubject, Observable} from 'rxjs'; import ApiResponse from '../structures/ApiResponse'; +import {MenuItem} from './db/MenuItem'; +import {DatabaseService} from './db/database.service'; +import {ConnectionService} from 'ng-connection-service'; @Injectable({ providedIn: 'root' @@ -11,10 +14,36 @@ export class ApiService { protected baseEndpoint: string = environment.backendBase; protected statUrl: string = environment.statUrl; protected versionUrl: string = environment.versionUrl; + protected offline = false; + public readonly offline$: BehaviorSubject = new BehaviorSubject(false); + + get isOffline() { + return this.offline; + } constructor( protected http: HttpClient, - ) { } + protected db: DatabaseService, + protected connection: ConnectionService, + ) { + connection.monitor().subscribe(isConnected => { + if ( !isConnected ) { + this.makeOffline(); + } else { + this.makeOnline(); // TODO add checks for server. + } + }); + } + + public makeOffline() { + this.offline = true; + this.offline$.next(true); + } + + public makeOnline() { + this.offline = false; + this.offline$.next(false); + } public get(endpoint, params = {}): Observable { return this.request(endpoint, params, 'get'); @@ -29,14 +58,55 @@ export class ApiService { } public stat(): Observable { - return this._request(this.statUrl); + return new Observable(sub => { + (async () => { + const statKV = await this.db.getKeyValue('host_stat'); + + // If offline, look up the last stored stat for information + if ( this.isOffline ) { + if ( typeof statKV !== 'object' ) { + throw new Error('No locally stored host stat found.'); + } + + sub.next(new ApiResponse(statKV.data)); + sub.complete(); + } + + // Otherwise, fetch the stat and cache it locally + this._request(this.statUrl).subscribe(apiResponse => { + statKV.data = {status: apiResponse.status, message: apiResponse.message, data: apiResponse.data}; + statKV.save().then(() => { + sub.next(statKV.data); + sub.complete(); + }); + }); + })(); + + }); } public version(): Promise { - return new Promise((res, rej) => { + return new Promise(async (res, rej) => { + const versionKV = await this.db.getKeyValue('app_version'); + + // If offline, look up the local app version. + if ( this.isOffline ) { + if ( versionKV ) { + return res(versionKV.data); + } else { + return rej(new Error('No local app version found.')); + } + } + + // Otherwise, look up the app version and store it locally this._request(this.versionUrl).subscribe({ - next: result => { - res(result.data.text.trim()); + next: async result => { + const version = result.data.text.trim(); + + versionKV.data = version; + await versionKV.save(); + + res(version); }, error: rej, }); @@ -76,4 +146,79 @@ export class ApiService { return `${this.baseEndpoint.endsWith('/') ? this.baseEndpoint.slice(0, -1) : this.baseEndpoint}${endpoint}`; } + + public getMenuItems(): Promise { + return new Promise(async (res, rej) => { + await this.db.createSchemata(); + + // If offline, fetch the menu from the database + if ( this.isOffline ) { + const items = await this.db.menuItems.toArray(); + const nodes = MenuItem.inflateTree(items as MenuItem[]); + return res(nodes); + } + + // Download the latest menu items + const tree: any[] = await new Promise(res2 => { + this.get('/menu/items').subscribe({ + next: async result => { + const nodes = result.data as any[]; + const items = MenuItem.deflateTree(nodes); + + // Update the locally stored nodes + await this.db.menuItems.clear(); + await Promise.all(items.map(item => item.save())); + + res2(nodes); + }, + error: rej, + }); + }); + + res(tree); + }); + } + + public getSessionData(): Promise { + return new Promise(async (res, rej) => { + const sessionKV = await this.db.getKeyValue('session_data'); + + // If offline, just return the locally cached session data + if ( this.isOffline ) { + if ( typeof sessionKV.data !== 'object' ) { + return rej(new Error('No locally cached session data found.')); + } + + return res(sessionKV.data); + } + + // Otherwise, fetch the session data from the server and cache it locally + this.get('/session').subscribe(async result => { + sessionKV.data = result.data; + await sessionKV.save(); + res(sessionKV.data); + }); + }); + } + + public saveSessionData(data: any): Promise { + return new Promise(async (res, rej) => { + // Update the local session data + const sessionKV = await this.db.getKeyValue('session_data'); + sessionKV.data = data; + await sessionKV.save(); + + // If we're not offline, then update the data on the server + if ( !this.isOffline ) { + await new Promise(res2 => { + this.post('/session', data || {}).subscribe({ + next: res2, + error: rej, + }); + }); + } + + res(); + }); + } } diff --git a/src/app/service/db/KeyValue.ts b/src/app/service/db/KeyValue.ts new file mode 100644 index 0000000..34bc195 --- /dev/null +++ b/src/app/service/db/KeyValue.ts @@ -0,0 +1,65 @@ +import {Model} from './Model'; + +export interface IKeyValue { + id?: number; + key: string; + value: string; + json: boolean; +} + +export class KeyValue extends Model implements IKeyValue { + id?: number; + key: string; + value: string; + json: boolean; + + public static getTableName() { + return 'keyValues'; + } + + public static getSchema() { + return '++id, key, value, json'; + } + + constructor(key: string, value: string, json: boolean, id?: number) { + super(); + + this.key = key; + this.value = value; + this.json = json; + if ( id ) { + this.id = id; + } + } + + get data(): any { + if ( this.json ) { + return JSON.parse(this.value); + } + + return this.value; + } + + set data(val: any) { + if ( typeof val === 'string' ) { + this.json = false; + this.value = val; + } else { + this.json = true; + this.value = JSON.stringify(val); + } + } + + public getSaveRecord(): any { + return { + ...(this.id ? { id: this.id } : {}), + key: this.key, + value: this.value, + json: this.json, + }; + } + + public getDatabase(): Dexie.Table { + return this.staticClass().dbService.table('keyValues') as Dexie.Table; + } +} diff --git a/src/app/service/db/MenuItem.ts b/src/app/service/db/MenuItem.ts new file mode 100644 index 0000000..ba11a77 --- /dev/null +++ b/src/app/service/db/MenuItem.ts @@ -0,0 +1,171 @@ +import {Model} from './Model'; +import Dexie from 'dexie'; + +export interface IMenuItem { + id?: number; + serverId: string; + name: string; + childIds?: string[]; + noDelete?: boolean; + noChildren?: boolean; + virtual?: boolean; + type?: string; + shared?: boolean; + needsServerUpdate?: boolean; +} + +export class MenuItem extends Model implements IMenuItem { + id?: number; + serverId: string; + name: string; + childIds?: string[]; + noDelete?: boolean; + noChildren?: boolean; + virtual?: boolean; + type?: string; + shared?: boolean; + needsServerUpdate?: boolean; + + public static getTableName() { + return 'menuItems'; + } + + public static getSchema() { + return '++id, serverId, name, childIds, noDelete, noChildren, virtual, type, shared, needsServerUpdate'; + } + + public static deflateTree(nodes: any[]): MenuItem[] { + let items = []; + + for ( const node of nodes ) { + const childIds = node.children ? node.children.map(x => x.id) : []; + const item = new MenuItem(node.name, node.id, childIds, node.noDelete, node.noChildren, node.virtual, node.type, node.shared); + + items.push(item); + if ( node.children ) { + items = items.concat(...this.deflateTree(node.children)); + } + } + + return items; + } + + public static inflateTree(items: MenuItem[]) { + const serverIdXItems: { [key: string]: MenuItem[] } = {}; + + for ( const item of items ) { + if ( !serverIdXItems[item.serverId] ) { + serverIdXItems[item.serverId] = []; + } + + serverIdXItems[item.serverId].push(item); + } + + const inflateNode = (item, alreadyChildren = [], seen = []) => { + const node: any = item.getSaveRecord(); + seen.push(item); + node.id = node.serverId; + + node.children = []; + if ( item.childIds ) { + for ( const childId of item.childIds ) { + if ( serverIdXItems[childId] ) { + const children = serverIdXItems[childId].filter(x => { + if ( x.type !== 'page' && item.serverId !== x.serverId ) { + return false; + } + + return x !== item && !alreadyChildren.includes(x) && !seen.includes(x); + }); + + node.children = node.children.concat(...children.map(x => inflateNode(x, children, seen))); + } + } + } + + const pageChildren = []; + const otherChildren = []; + for ( const child of node.children ) { + if ( child.type === 'page' ) { + pageChildren.push(child); + } else { + otherChildren.push(child); + } + } + + node.children = [...otherChildren, ...pageChildren]; + return node; + }; + + const topLevelItems = items.filter(x => String(x.serverId) === '0'); + return topLevelItems.map(x => inflateNode(x)); + } + + constructor( + name: string, + serverId: string, + childIds?: string[], + noDelete?: boolean, + noChildren?: boolean, + virtual?: boolean, + type?: string, + shared?: boolean, + needsServerUpdate?: boolean, + id?: number + ) { + super(); + + this.name = name; + this.serverId = serverId; + if ( childIds ) { + this.childIds = childIds; + } + + if ( typeof noDelete !== 'undefined' ) { + this.noDelete = noDelete; + } + + if ( typeof noChildren !== 'undefined' ) { + this.noChildren = noChildren; + } + + if ( typeof virtual !== 'undefined' ) { + this.virtual = virtual; + } + + if ( type ) { + this.type = type; + } + + if ( typeof shared !== 'undefined' ) { + this.shared = shared; + } + + if ( typeof needsServerUpdate !== 'undefined' ) { + this.needsServerUpdate = needsServerUpdate; + } + + if ( id ) { + this.id = id; + } + } + + public getDatabase(): Dexie.Table { + return this.staticClass().dbService.table('menuItems') as Dexie.Table; + } + + public getSaveRecord(): any { + return { + ...(this.id ? { id: this.id } : {}), + serverId: this.serverId, + name: this.name, + ...(typeof this.childIds !== 'undefined' ? { childIds: this.childIds } : {}), + ...(typeof this.noDelete !== 'undefined' ? { noDelete: this.noDelete } : {}), + ...(typeof this.noChildren !== 'undefined' ? { noChildren: this.noChildren } : {}), + ...(typeof this.virtual !== 'undefined' ? { virtual: this.virtual } : {}), + ...(typeof this.type !== 'undefined' ? { type: this.type } : {}), + ...(typeof this.shared !== 'undefined' ? { shared: this.shared } : {}), + ...(typeof this.needsServerUpdate !== 'undefined' ? { needsServerUpdate: this.needsServerUpdate } : {}), + }; + } +} diff --git a/src/app/service/db/Migration.ts b/src/app/service/db/Migration.ts new file mode 100644 index 0000000..11d5241 --- /dev/null +++ b/src/app/service/db/Migration.ts @@ -0,0 +1,43 @@ +import {Model} from './Model'; + +export interface IMigration { + id?: number; + uuid: string; + applied: boolean; +} + +export class Migration extends Model implements IMigration { + id?: number; + uuid: string; + applied: boolean; + + public static getTableName() { + return 'migrations'; + } + + public static getSchema() { + return '++id, uuid, applied'; + } + + constructor(uuid: string, applied: boolean, id?: number) { + super(); + + this.uuid = uuid; + this.applied = applied; + if ( id ) { + this.id = id; + } + } + + public getSaveRecord(): any { + return { + ...(this.id ? { id: this.id } : {}), + uuid: this.uuid, + applied: this.applied, + }; + } + + public getDatabase(): Dexie.Table { + return this.staticClass().dbService.table('migrations') as Dexie.Table; + } +} diff --git a/src/app/service/db/Model.ts b/src/app/service/db/Model.ts new file mode 100644 index 0000000..f7bbb55 --- /dev/null +++ b/src/app/service/db/Model.ts @@ -0,0 +1,31 @@ +import Dexie from 'dexie'; +import {DatabaseService} from './database.service'; + +export abstract class Model { + public static dbService?: DatabaseService; + + public id?: number; + + public static getSchema(): string { + throw new TypeError('Child class must implement.'); + } + + public static getTableName(): string { + throw new TypeError('Child class must implement.'); + } + + public abstract getDatabase(): Dexie.Table; + public abstract getSaveRecord(): any; + + public staticClass() { + return (this.constructor as typeof Model); + } + + public exists() { + return !!this.id; + } + + public async save() { + this.id = await this.getDatabase().put(this.getSaveRecord()); + } +} diff --git a/src/app/service/db/database.service.ts b/src/app/service/db/database.service.ts new file mode 100644 index 0000000..8e69950 --- /dev/null +++ b/src/app/service/db/database.service.ts @@ -0,0 +1,67 @@ +import { Injectable } from '@angular/core'; +import Dexie from 'dexie'; +import {IMigration, Migration} from './Migration'; +import {IMenuItem, MenuItem} from './MenuItem'; +import {KeyValue, IKeyValue} from './KeyValue'; + +@Injectable({ + providedIn: 'root' +}) +export class DatabaseService extends Dexie { + protected static registeredModels = [Migration, MenuItem, KeyValue]; + protected initialized = false; + + migrations!: Dexie.Table; + menuItems!: Dexie.Table; + keyValues!: Dexie.Table; + + constructor( + ) { + super('NodedLocalDatabase'); + } + + public async getKeyValue(key: string): Promise { + const matches = await this.keyValues.where({ key }).toArray(); + + if ( matches.length > 0 ) { + return matches[0] as KeyValue; + } + + return new KeyValue(key, '', false); + } + + public async createSchemata() { + if ( this.initialized ) { + return; + } + this.initialized = true; + + console.log('db', this); + + const staticClass = this.constructor as typeof DatabaseService; + const schema: any = {}; + + for ( const ModelClass of staticClass.registeredModels ) { + ModelClass.dbService = this; + schema[ModelClass.getTableName()] = ModelClass.getSchema(); + } + + await this.version(3).stores(schema); + await this.open(); + + this.migrations = this.table('migrations'); + this.migrations.mapToClass(Migration); + + this.menuItems = this.table('menuItems'); + this.menuItems.mapToClass(MenuItem); + + this.keyValues = this.table('keyValues'); + this.keyValues.mapToClass(KeyValue); + + // await new Promise(res => { + // setTimeout(() => { + // res(); + // }, 1000); + // }); + } +} diff --git a/src/app/service/session.service.ts b/src/app/service/session.service.ts index d820600..2d466c7 100644 --- a/src/app/service/session.service.ts +++ b/src/app/service/session.service.ts @@ -50,28 +50,13 @@ export class SessionService { } async initialize() { - return new Promise((res, rej) => { - this.api.get('/session').subscribe(response => { - this.data = response.data; - res(); - }); - }); + this.data = await this.api.getSessionData(); } async save() { this.saving = true; - return new Promise((res, rej) => { - this.api.post('/session', this.data || {}).subscribe({ - next: result => { - res(); - this.saving = false; - }, - error: (e) => { - this.saving = false; - rej(e); - }, - }); - }); + await this.api.saveSessionData(this.data); + this.saving = false; } buildAppUrl(...parts: string[]): string {