diff --git a/.gitignore b/.gitignore index ad2ed3c6..7408f302 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,6 @@ node_modules .angular packages/ngx-web-component/**/generated dist -www \ No newline at end of file +www +.nx +.python-version \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 86ec1364..e582b7b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12377,6 +12377,12 @@ "integrity": "sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==", "dev": true }, + "node_modules/@types/unidecode": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@types/unidecode/-/unidecode-0.1.3.tgz", + "integrity": "sha512-7R8zgAf8y1qq5Zif6UIXYR07MHvJIjcQM9Ym2am1YXaWdn9zJltLDwO8HpmIIjHiNT4VMGiNAw+UI9S7OM2foA==", + "dev": true + }, "node_modules/@types/ws": { "version": "8.5.4", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.4.tgz", @@ -15249,8 +15255,7 @@ "node_modules/core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", - "dev": true + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==" }, "node_modules/cors": { "version": "2.8.5", @@ -19606,6 +19611,11 @@ "node": ">=0.10.0" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" + }, "node_modules/immutable": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.0.tgz", @@ -19693,8 +19703,7 @@ "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "node_modules/ini": { "version": "3.0.1", @@ -25255,6 +25264,49 @@ "verror": "1.10.0" } }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jszip/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "node_modules/jszip/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/jszip/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/jszip/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/just-diff": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/just-diff/-/just-diff-5.2.0.tgz", @@ -25826,6 +25878,14 @@ } } }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/lilconfig": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", @@ -28778,8 +28838,7 @@ "node_modules/pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", - "dev": true + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" }, "node_modules/parent-module": { "version": "1.0.1", @@ -30590,8 +30649,7 @@ "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, "node_modules/promise-all-reject-late": { "version": "1.0.1", @@ -31937,6 +31995,11 @@ "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", "dev": true }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -34079,6 +34142,14 @@ "node": ">=4" } }, + "node_modules/unidecode": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/unidecode/-/unidecode-1.0.1.tgz", + "integrity": "sha512-9t2iq9jV5+FtXDDyNwMk6Tm0UjoOahc2aqA8B5gG0ED/DsFWHiMdKvEF/R+1gLGFHuzWaNZjbfj+Hf0KQYz+Yg==", + "engines": { + "node": ">= 0.4.12" + } + }, "node_modules/union": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/union/-/union-0.5.0.tgz", @@ -34212,8 +34283,7 @@ "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "node_modules/utils-merge": { "version": "1.0.1", @@ -35317,7 +35387,7 @@ }, "packages/ngx-web-component": { "name": "@readalongs/ngx-web-component", - "version": "1.1.1", + "version": "1.2.0", "devDependencies": { "@angular-devkit/architect": "^0.1501.6", "@angular-devkit/build-angular": "^15.2.4", @@ -35357,12 +35427,15 @@ "bootstrap": "^5.2.3", "file-saver": "^2.0.5", "image-conversion": "^2.1.1", + "jszip": "^3.10.1", + "mime": "^4.0.1", "ngx-toastr": "^16.1.0", "rxjs": "~7.5.0", "shepherd.js": "^11.0.1", "soundswallower": "^0.6.3", "standardized-audio-context": "^25.3.41", "tslib": "^2.3.0", + "unidecode": "^1.0.1", "zone.js": "^0.11.8" }, "devDependencies": { @@ -35374,6 +35447,7 @@ "@angular/localize": "^15.2.4", "@types/dom-mediacapture-record": "^1.0.15", "@types/jasmine": "~4.0.0", + "@types/unidecode": "^0.1.3", "jasmine-core": "~4.1.0", "karma": "~6.3.0", "karma-chrome-launcher": "~3.1.0", @@ -35464,9 +35538,23 @@ "rxjs": "^6.5.3 || ^7.4.0" } }, + "packages/studio-web/node_modules/mime": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/mime/-/mime-4.0.1.tgz", + "integrity": "sha512-5lZ5tyrIfliMXzFtkYyekWbtRXObT9OWa8IwQ5uxTBDHucNNwniRqo0yInflj+iYi5CBa6qxadGzGarDfuEOxA==", + "funding": [ + "https://github.com/sponsors/broofa" + ], + "bin": { + "mime": "bin/cli.js" + }, + "engines": { + "node": ">=16" + } + }, "packages/web-component": { "name": "@readalongs/web-component", - "version": "1.1.1", + "version": "1.2.0", "license": "MIT", "dependencies": { "audio-recorder-polyfill": "^0.4.1", @@ -45235,6 +45323,12 @@ "integrity": "sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==", "dev": true }, + "@types/unidecode": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@types/unidecode/-/unidecode-0.1.3.tgz", + "integrity": "sha512-7R8zgAf8y1qq5Zif6UIXYR07MHvJIjcQM9Ym2am1YXaWdn9zJltLDwO8HpmIIjHiNT4VMGiNAw+UI9S7OM2foA==", + "dev": true + }, "@types/ws": { "version": "8.5.4", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.4.tgz", @@ -47400,8 +47494,7 @@ "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", - "dev": true + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==" }, "cors": { "version": "2.8.5", @@ -50696,6 +50789,11 @@ "dev": true, "optional": true }, + "immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" + }, "immutable": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.0.tgz", @@ -50761,8 +50859,7 @@ "inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "ini": { "version": "3.0.1", @@ -55008,6 +55105,51 @@ "verror": "1.10.0" } }, + "jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "requires": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, "just-diff": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/just-diff/-/just-diff-5.2.0.tgz", @@ -55444,6 +55586,14 @@ "webpack-sources": "^3.0.0" } }, + "lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "requires": { + "immediate": "~3.0.5" + } + }, "lilconfig": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", @@ -57686,8 +57836,7 @@ "pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", - "dev": true + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" }, "parent-module": { "version": "1.0.1", @@ -58866,8 +59015,7 @@ "process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, "promise-all-reject-late": { "version": "1.0.1", @@ -59309,15 +59457,18 @@ "@types/emscripten": "^1.39.6", "@types/file-saver": "^2.0.5", "@types/jasmine": "~4.0.0", + "@types/unidecode": "^0.1.3", "bootstrap": "^5.2.3", "file-saver": "^2.0.5", "image-conversion": "^2.1.1", "jasmine-core": "~4.1.0", + "jszip": "^3.10.1", "karma": "~6.3.0", "karma-chrome-launcher": "~3.1.0", "karma-coverage": "~2.2.0", "karma-jasmine": "~5.0.0", "karma-jasmine-html-reporter": "~1.7.0", + "mime": "^4.0.1", "ngx-toastr": "^16.1.0", "rxjs": "~7.5.0", "shepherd.js": "^11.0.1", @@ -59325,6 +59476,7 @@ "standardized-audio-context": "^25.3.41", "tslib": "^2.3.0", "typescript": "^4.9.5", + "unidecode": "^1.0.1", "webpack": "^5.77.0", "zone.js": "^0.11.8" }, @@ -59392,6 +59544,11 @@ "@material/typography": "15.0.0-canary.684e33d25.0", "tslib": "^2.3.0" } + }, + "mime": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/mime/-/mime-4.0.1.tgz", + "integrity": "sha512-5lZ5tyrIfliMXzFtkYyekWbtRXObT9OWa8IwQ5uxTBDHucNNwniRqo0yInflj+iYi5CBa6qxadGzGarDfuEOxA==" } } }, @@ -60003,6 +60160,11 @@ "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", "dev": true }, + "setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" + }, "setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -61594,6 +61756,11 @@ "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", "dev": true }, + "unidecode": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/unidecode/-/unidecode-1.0.1.tgz", + "integrity": "sha512-9t2iq9jV5+FtXDDyNwMk6Tm0UjoOahc2aqA8B5gG0ED/DsFWHiMdKvEF/R+1gLGFHuzWaNZjbfj+Hf0KQYz+Yg==" + }, "union": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/union/-/union-0.5.0.tgz", @@ -61689,8 +61856,7 @@ "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "utils-merge": { "version": "1.0.1", diff --git a/packages/ngx-web-component/package.json b/packages/ngx-web-component/package.json index 371d72be..583a314d 100644 --- a/packages/ngx-web-component/package.json +++ b/packages/ngx-web-component/package.json @@ -1,7 +1,7 @@ { "name": "@readalongs/ngx-web-component", "type": "module", - "version": "1.1.1", + "version": "1.2.0", "peerDependencies": { "@angular/animations": "^15.2.4", "@angular/common": "^15.2.4", diff --git a/packages/studio-web/package.json b/packages/studio-web/package.json index c84f1d80..dab908ef 100644 --- a/packages/studio-web/package.json +++ b/packages/studio-web/package.json @@ -21,6 +21,7 @@ "@angular/localize": "^15.2.4", "@types/dom-mediacapture-record": "^1.0.15", "@types/jasmine": "~4.0.0", + "@types/unidecode": "^0.1.3", "jasmine-core": "~4.1.0", "karma": "~6.3.0", "karma-chrome-launcher": "~3.1.0", @@ -46,14 +47,17 @@ "bootstrap": "^5.2.3", "file-saver": "^2.0.5", "image-conversion": "^2.1.1", + "jszip": "^3.10.1", + "mime": "^4.0.1", "ngx-toastr": "^16.1.0", "rxjs": "~7.5.0", "shepherd.js": "^11.0.1", "soundswallower": "^0.6.3", "standardized-audio-context": "^25.3.41", "tslib": "^2.3.0", + "unidecode": "^1.0.1", "zone.js": "^0.11.8" }, - "singleFileBundleVersion": "1.1.0", - "singleFileBundleTimestamp": "2023-10-23+21-05-11" + "singleFileBundleVersion": "1.2.0", + "singleFileBundleTimestamp": "2024-04-02+15-07-59" } diff --git a/packages/studio-web/src/app/audio.service.ts b/packages/studio-web/src/app/audio.service.ts deleted file mode 100644 index 8515e444..00000000 --- a/packages/studio-web/src/app/audio.service.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { from, Observable, sample } from "rxjs"; - -import { Injectable } from "@angular/core"; -import { AudioContext, AudioBuffer } from "standardized-audio-context"; - -@Injectable({ - providedIn: "root", -}) -export class AudioService { - constructor() {} - - loadAudioBufferFromFile$( - file: File, - sampleRate: number - ): Observable { - var audioCtx = new AudioContext({ sampleRate }); - var audioFile = file.arrayBuffer().then((buffer: any) => { - return audioCtx.decodeAudioData(buffer); - }); - return from(audioFile); - } -} diff --git a/packages/studio-web/src/app/b64.service.ts b/packages/studio-web/src/app/b64.service.ts index e6cb285d..7d52b500 100644 --- a/packages/studio-web/src/app/b64.service.ts +++ b/packages/studio-web/src/app/b64.service.ts @@ -1,11 +1,10 @@ import { forkJoin, Observable } from "rxjs"; -import { map, switchMap } from "rxjs/operators"; +import { switchMap } from "rxjs/operators"; import { HttpClient } from "@angular/common/http"; import { Injectable } from "@angular/core"; import { FileService } from "./file.service"; -import { Segment } from "soundswallower"; @Injectable({ providedIn: "root", @@ -13,6 +12,11 @@ import { Segment } from "soundswallower"; export class B64Service { JS_BUNDLE_URL = "assets/bundle.js"; FONTS_BUNDLE_URL = "assets/fonts.b64.css"; + /** + * Creates an instance of B64Service, a service for B64 encoding assets. + * @param {HttpClient} http - The HttpClient service for making HTTP requests. + * @param {FileService} fileService - The FileService for handling file operations. + */ constructor(private http: HttpClient, private fileService: FileService) {} getBundle$(): Observable { return forkJoin([ diff --git a/packages/studio-web/src/app/demo/demo.component.spec.ts b/packages/studio-web/src/app/demo/demo.component.spec.ts index 800c3c01..118b7773 100644 --- a/packages/studio-web/src/app/demo/demo.component.spec.ts +++ b/packages/studio-web/src/app/demo/demo.component.spec.ts @@ -1,12 +1,7 @@ import { CUSTOM_ELEMENTS_SCHEMA } from "@angular/core"; import { ComponentFixture, TestBed } from "@angular/core/testing"; import { ToastrModule } from "ngx-toastr"; -import { - HttpClientTestingModule, - HttpTestingController, -} from "@angular/common/http/testing"; -// add By to query -import { By } from "@angular/platform-browser"; +import { HttpClientTestingModule } from "@angular/common/http/testing"; import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; import { MaterialModule } from "../material.module"; diff --git a/packages/studio-web/src/app/demo/demo.component.ts b/packages/studio-web/src/app/demo/demo.component.ts index 56766b86..e43c92b9 100644 --- a/packages/studio-web/src/app/demo/demo.component.ts +++ b/packages/studio-web/src/app/demo/demo.component.ts @@ -6,11 +6,20 @@ import { Components } from "@readalongs/web-component/loader"; import { HttpErrorResponse } from "@angular/common/http"; import { B64Service } from "../b64.service"; +import { slugify } from "../utils/utils"; import { compress } from "image-conversion"; import { RasService, SupportedOutputs } from "../ras.service"; import { saveAs } from "file-saver"; import { environment } from "../../environments/environment"; +import { UploadService } from "../upload.service"; +import JSZip from "jszip"; +import mime from "mime"; + +interface Image { + path: string; + blob: Blob; +} @Component({ selector: "app-demo", @@ -27,6 +36,7 @@ export class DemoComponent implements OnDestroy, OnInit { }; outputFormats = [ { value: "html", display: $localize`Offline HTML` }, + { value: "zip", display: $localize`Web Bundle` }, { value: "eaf", display: $localize`Elan File` }, { value: "textgrid", display: $localize`Praat TextGrid` }, { value: "srt", display: $localize`SRT Subtitles` }, @@ -35,10 +45,31 @@ export class DemoComponent implements OnDestroy, OnInit { selectedOutputFormat: SupportedOutputs | string = "html"; language: "eng" | "fra" | "spa" = "eng"; unsubscribe$ = new Subject(); + xmlSerializer = new XMLSerializer(); + readmeFile = new Blob( + [ + `Web Deployment Guide + +This bundle has everything you need to host your ReadAlong on your own server. + +Your audio, (optional) image, and alignment (.readalong) assets are stored in the assets folder. + +The plain text used to create your ReadAlong is also stored here along with an example index.html file. + +Your index.html file demonstrates the snippet and imports needed to host the ReadAlong on your server. + +Please host all assets on your server, include the font and package imports defined in the index.html in your website's imports, and include the corresponding snippet everywhere you would like your ReadAlong to be displayed. + `, + ], + { + type: "text/plain", + } + ); constructor( public b64Service: B64Service, private rasService: RasService, - private toastr: ToastrService + private toastr: ToastrService, + private uploadService: UploadService ) { // If we do more languages, this should be a lookup table if ($localize.locale == "fr") { @@ -123,9 +154,14 @@ export class DemoComponent implements OnDestroy, OnInit { } } - async updateImages(doc: Document): Promise { + async updateImages( + doc: Document, + b64Embed = true, + imagePrefix = "image" + ): Promise { const images = await this.readalong.getImages(); const page_nodes = doc.querySelectorAll("div[type=page]"); + const imageBlobs: Image[] = []; for (const [i, img] of Object.entries(images)) { let currentPage = page_nodes[parseInt(i)]; // Add Image @@ -136,16 +172,29 @@ export class DemoComponent implements OnDestroy, OnInit { // @ts-ignore let blob = await fetch(img).then((r) => r.blob()); blob = await compress(blob, 0.75); - let b64 = await this.b64Service.blobToB64(blob); - // @ts-ignore - graphic.setAttribute("url", b64); + // Either embed the images directly + if (b64Embed) { + let b64 = await this.b64Service.blobToB64(blob); + // @ts-ignore + graphic.setAttribute("url", b64); + // or return a list of blobs and use the filename here + } else { + const extension = mime.getExtension(blob.type); + const path = `${imagePrefix}-${i}.${extension}`; + imageBlobs.push({ blob: blob, path: path }); + graphic.setAttribute("url", `${path}`); + } currentPage.appendChild(graphic); // Remove Images } else if (img === null) { currentPage.querySelectorAll("graphic").forEach((e) => e.remove()); } } - return true; + if (b64Embed) { + return true; + } else { + return imageBlobs; + } } registerDownloadEvent() { @@ -160,6 +209,13 @@ export class DemoComponent implements OnDestroy, OnInit { if (this.selectedOutputFormat == "html") { await this.updateImages(ras); await this.updateTranslations(ras); + const timestamp = new Date() + .toISOString() + .replace(/[^0-9]/g, "") + .slice(0, -3); + const basename = + (this.slots.title ? slugify(this.slots.title, 15) : "readalong") + + `-${timestamp}`; let b64ras = this.b64Service.xmlToB64(ras); var element = document.createElement("a"); let blob = new Blob( @@ -174,7 +230,7 @@ export class DemoComponent implements OnDestroy, OnInit { - + ${this.slots.title} ${this.slots.subtitle} @@ -184,11 +240,99 @@ export class DemoComponent implements OnDestroy, OnInit { { type: "text/html;charset=utf-8" } ); element.href = window.URL.createObjectURL(blob); - element.download = "readalong.html"; + + element.download = `${basename}.html`; document.body.appendChild(element); element.click(); document.body.removeChild(element); this.registerDownloadEvent(); + } else if (this.selectedOutputFormat === "zip") { + let zipFile = new JSZip(); + // Create inner folder + const innerFolder = zipFile.folder("readalong"); + const assetsFolder = innerFolder?.folder("assets"); + const timestamp = new Date() + .toISOString() + .replace(/[^0-9]/g, "") + .slice(0, -3); + const basename = + (this.slots.title ? slugify(this.slots.title, 15) : "readalong") + + `-${timestamp}`; + // - add audio file + if (this.uploadService.$currentAudio.value !== null) { + // Recorded audio is always mp3 + let audioExtension = "mp3"; + // If the audio is a file, just pull the extension + if (this.uploadService.$currentAudio.value instanceof File) { + const file_parts = + this.uploadService.$currentAudio.value.name.split("."); + audioExtension = file_parts[file_parts.length - 1]; + } + assetsFolder?.file( + `${basename}.${audioExtension}`, + this.uploadService.$currentAudio.value + ); + } + // - add images + // @ts-ignore + const images: Image[] = await this.updateImages( + ras, + false, + `image-${basename}` + ); + for (let image of images) { + assetsFolder?.file(image.path, image.blob); + } + // - add plain text file + if (this.uploadService.$currentText.value !== null) { + innerFolder?.file( + `${basename}.txt`, + this.uploadService.$currentText.value + ); + } + // - add .readalong file + this.updateTranslations(ras); + + const xmlString = this.xmlSerializer.serializeToString( + ras.documentElement + ); + const rasFile = new Blob([xmlString], { type: "application/xml" }); + assetsFolder?.file(`${basename}.readalong`, rasFile); + // - add index.html file + const sampleHtml = ` + + + + + ${this.slots.title} + + + + + + + + ${this.slots.title} + ${this.slots.subtitle} + + + + + + + `; + const indexHtmlFile = new Blob([sampleHtml], { type: "text/html" }); + innerFolder?.file("index.html", indexHtmlFile); + // - add plain text readme + innerFolder?.file("README.txt", this.readmeFile); + // - write zip + zipFile.generateAsync({ type: "blob" }).then( + (content) => saveAs(content, `${basename}.zip`), + (err: HttpErrorResponse) => + this.toastr.error(err.error.detail, $localize`Download failed.`, { + timeOut: 30000, + }) + ); } else { let audio: HTMLAudioElement = new Audio(this.b64Inputs[0]); this.rasService diff --git a/packages/studio-web/src/app/file.service.ts b/packages/studio-web/src/app/file.service.ts index c356f9f2..900becb0 100644 --- a/packages/studio-web/src/app/file.service.ts +++ b/packages/studio-web/src/app/file.service.ts @@ -1,7 +1,8 @@ -import { Observable, catchError, map, of, take } from "rxjs"; +import { Observable, catchError, from, map, of, take } from "rxjs"; import { HttpClient, HttpErrorResponse } from "@angular/common/http"; import { Injectable } from "@angular/core"; import { ToastrService } from "ngx-toastr"; +import { AudioContext, AudioBuffer } from "standardized-audio-context"; @Injectable({ providedIn: "root", @@ -9,6 +10,17 @@ import { ToastrService } from "ngx-toastr"; export class FileService { constructor(private http: HttpClient, private toastr: ToastrService) {} + loadAudioBufferFromFile$( + file: File, + sampleRate: number + ): Observable { + var audioCtx = new AudioContext({ sampleRate }); + var audioFile = file.arrayBuffer().then((buffer: any) => { + return audioCtx.decodeAudioData(buffer); + }); + return from(audioFile); + } + returnFileFromPath$ = (url: string, responseType: string = "blob") => { const httpOptions: Object = { responseType }; return this.http.get(url, httpOptions).pipe( diff --git a/packages/studio-web/src/app/soundswallower.service.spec.ts b/packages/studio-web/src/app/soundswallower.service.spec.ts index e0652d59..3dc20c5d 100644 --- a/packages/studio-web/src/app/soundswallower.service.spec.ts +++ b/packages/studio-web/src/app/soundswallower.service.spec.ts @@ -1,8 +1,13 @@ /* -*- typescript-indent-level: 2 -*- */ import { TestBed } from "@angular/core/testing"; -import { last, of, lastValueFrom, concat } from "rxjs"; import { SoundswallowerService } from "./soundswallower.service"; -import { AudioService } from "./audio.service"; +import { ToastrModule } from "ngx-toastr"; +import { FileService } from "./file.service"; +import { HttpClient } from "@angular/common/http"; +import { + HttpClientTestingModule, + HttpTestingController, +} from "@angular/common/http/testing"; const b64audio = `data:audio/wave;base64,UklGRiSAAABXQVZFZm10IBAAAAABAAEAQB8AAIA+AAACABAAZGF0YQCAAAAzAGIAVgBdAGIAXwBv AG8AYwBaAGMAZwBZAGUAcQBxAIYAiwCBAHUAeAB7AG4AegB6AHsAewCDAI0AhQCUAJ0AkwCXAJoA @@ -583,12 +588,19 @@ AFwAWQBbAGEAagBvAHAAcABkAGIAaABhAGcAagBwAGkAVwBfAA==`; describe("SoundswallowerService", () => { let service: SoundswallowerService; - let audioService: AudioService; - + let fileService: FileService; + let httpClientSpy: jasmine.SpyObj; + let httpClient: HttpClient; + let httpTestingController: HttpTestingController; beforeEach(() => { - TestBed.configureTestingModule({}); + httpClientSpy = jasmine.createSpyObj("HttpClient", ["get"]); + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, ToastrModule.forRoot()], + }); + httpClient = TestBed.inject(HttpClient); + httpTestingController = TestBed.inject(HttpTestingController); service = TestBed.inject(SoundswallowerService); - audioService = TestBed.inject(AudioService); + fileService = TestBed.inject(FileService); }); it("should be created", () => { @@ -598,7 +610,7 @@ describe("SoundswallowerService", () => { /* All of these are commented out, because Karma cannot serve * arbitrary binary files, or if it can, there is ZERO DOCUMENTATION * telling you how to do that. */ - + // it("should be initialized", async () => { // const done = await lastValueFrom(concat(service.waitForInit$(), // of(true))); diff --git a/packages/studio-web/src/app/studio/studio.component.ts b/packages/studio-web/src/app/studio/studio.component.ts index 4c1987cd..d9df096c 100644 --- a/packages/studio-web/src/app/studio/studio.component.ts +++ b/packages/studio-web/src/app/studio/studio.component.ts @@ -49,9 +49,6 @@ import { HttpErrorResponse } from "@angular/common/http"; export class StudioComponent implements OnDestroy, OnInit { firstFormGroup: any; title = "readalong-studio"; - alignment = new Subject(); - text = new Subject(); - audio = new Subject(); b64Inputs$ = new Subject<[string, Document, [string, string]]>(); render$ = new BehaviorSubject(false); @ViewChild("upload", { static: false }) upload?: UploadComponent; @@ -150,9 +147,9 @@ export class StudioComponent implements OnDestroy, OnInit { formIsDirty() { return ( - this.upload?.audioControl.value !== null || - this.upload?.textControl.value !== null || - this.upload?.textInput + this.upload?.audioControl$.value !== null || + this.upload?.textControl$.value !== null || + this.upload?.$textInput ); } @@ -203,9 +200,9 @@ export class StudioComponent implements OnDestroy, OnInit { .pipe(takeUntil(this.unsubscribe$)) .subscribe((audioFile) => { if (!(audioFile instanceof HttpErrorResponse) && this.upload) { - this.upload.textInput = "Hello world!"; + this.upload.$textInput.next("Hello world!"); this.upload.inputMethod.text = "edit"; - this.upload.audioControl.setValue(audioFile); + this.upload.audioControl$.setValue(audioFile); this.upload?.nextStep(); this.stepper.animationDone.pipe(take(1)).subscribe(() => { // We can only attach to the shadow dom once it's been created, so unfortunately we need to define the steps like this. diff --git a/packages/studio-web/src/app/audio.service.spec.ts b/packages/studio-web/src/app/upload.service.spec.ts similarity index 55% rename from packages/studio-web/src/app/audio.service.spec.ts rename to packages/studio-web/src/app/upload.service.spec.ts index 0adbb2ba..b1a0dc21 100644 --- a/packages/studio-web/src/app/audio.service.spec.ts +++ b/packages/studio-web/src/app/upload.service.spec.ts @@ -1,13 +1,13 @@ import { TestBed } from "@angular/core/testing"; -import { AudioService } from "./audio.service"; +import { UploadService } from "./upload.service"; -describe("AudioService", () => { - let service: AudioService; +describe("UploadService", () => { + let service: UploadService; beforeEach(() => { TestBed.configureTestingModule({}); - service = TestBed.inject(AudioService); + service = TestBed.inject(UploadService); }); it("should be created", () => { diff --git a/packages/studio-web/src/app/upload.service.ts b/packages/studio-web/src/app/upload.service.ts new file mode 100644 index 00000000..79b3a280 --- /dev/null +++ b/packages/studio-web/src/app/upload.service.ts @@ -0,0 +1,11 @@ +import { Injectable } from "@angular/core"; +import { BehaviorSubject } from "rxjs"; + +@Injectable({ + providedIn: "root", +}) +export class UploadService { + $currentAudio = new BehaviorSubject(null); + $currentText = new BehaviorSubject(null); + constructor() {} +} diff --git a/packages/studio-web/src/app/upload/upload.component.html b/packages/studio-web/src/app/upload/upload.component.html index eb973b64..4ebe0ac1 100644 --- a/packages/studio-web/src/app/upload/upload.component.html +++ b/packages/studio-web/src/app/upload/upload.component.html @@ -78,7 +78,7 @@

Text