diff --git a/package-lock.json b/package-lock.json index 944752888..2c56f638d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3618,7 +3618,6 @@ "version": "7.9.6", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.9.6.tgz", "integrity": "sha512-64AF1xY3OAkFHqOb9s4jpgk1Mm5vDZ4L3acHvAml+53nO1XbXLuDodsVpO4OIUsmemlUHMxNdYMNJmsvOwLrvQ==", - "dev": true, "requires": { "regenerator-runtime": "^0.13.4" } @@ -9255,6 +9254,71 @@ "eslint-visitor-keys": "^2.0.0" } }, + "@videojs/http-streaming": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/@videojs/http-streaming/-/http-streaming-2.15.1.tgz", + "integrity": "sha512-/uuN3bVkEeJAdrhu5Hyb19JoUo3CMys7yf2C1vUjeL1wQaZ4Oe8JrZzRrnWZ0rjvPgKfNLPXQomsRtgrMoRMJQ==", + "requires": { + "@babel/runtime": "^7.12.5", + "@videojs/vhs-utils": "3.0.5", + "aes-decrypter": "3.1.3", + "global": "^4.4.0", + "m3u8-parser": "4.8.0", + "mpd-parser": "^0.22.1", + "mux.js": "6.0.1", + "video.js": "^6 || ^7" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.7.tgz", + "integrity": "sha512-UF0tvkUtxwAgZ5W/KrkHf0Rn0fdnLDU9ScxBrEVNUprE/MzirjK4MJUX1/BVDv00Sv8cljtukVK1aky++X1SjQ==", + "requires": { + "regenerator-runtime": "^0.13.11" + } + }, + "regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" + } + } + }, + "@videojs/vhs-utils": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-3.0.5.tgz", + "integrity": "sha512-PKVgdo8/GReqdx512F+ombhS+Bzogiofy1LgAj4tN8PfdBx3HSS7V5WfJotKTqtOWGwVfSWsrYN/t09/DSryrw==", + "requires": { + "@babel/runtime": "^7.12.5", + "global": "^4.4.0", + "url-toolkit": "^2.2.1" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.7.tgz", + "integrity": "sha512-UF0tvkUtxwAgZ5W/KrkHf0Rn0fdnLDU9ScxBrEVNUprE/MzirjK4MJUX1/BVDv00Sv8cljtukVK1aky++X1SjQ==", + "requires": { + "regenerator-runtime": "^0.13.11" + } + }, + "regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" + } + } + }, + "@videojs/xhr": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@videojs/xhr/-/xhr-2.6.0.tgz", + "integrity": "sha512-7J361GiN1tXpm+gd0xz2QWr3xNWBE+rytvo8J3KuggFaLg+U37gZQ2BuPLcnkfGffy2e+ozY70RHC8jt7zjA6Q==", + "requires": { + "@babel/runtime": "^7.5.5", + "global": "~4.4.0", + "is-function": "^1.0.1" + } + }, "@webassemblyjs/ast": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz", @@ -9430,6 +9494,11 @@ "@xtuc/long": "4.2.2" } }, + "@xmldom/xmldom": { + "version": "0.8.6", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.6.tgz", + "integrity": "sha512-uRjjusqpoqfmRkTaNuLJ2VohVr67Q5YwDATW3VU7PfzTj6IRaihGrYI7zckGZjxQPBIp63nfvJbM+Yu5ICh0Bg==" + }, "@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", @@ -9567,6 +9636,32 @@ } } }, + "aes-decrypter": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/aes-decrypter/-/aes-decrypter-3.1.3.tgz", + "integrity": "sha512-VkG9g4BbhMBy+N5/XodDeV6F02chEk9IpgRTq/0bS80y4dzy79VH2Gtms02VXomf3HmyRe3yyJYkJ990ns+d6A==", + "requires": { + "@babel/runtime": "^7.12.5", + "@videojs/vhs-utils": "^3.0.5", + "global": "^4.4.0", + "pkcs7": "^1.0.4" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.7.tgz", + "integrity": "sha512-UF0tvkUtxwAgZ5W/KrkHf0Rn0fdnLDU9ScxBrEVNUprE/MzirjK4MJUX1/BVDv00Sv8cljtukVK1aky++X1SjQ==", + "requires": { + "regenerator-runtime": "^0.13.11" + } + }, + "regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" + } + } + }, "agent-base": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", @@ -15674,8 +15769,7 @@ "dom-walk": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz", - "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==", - "dev": true + "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==" }, "domain-browser": { "version": "1.2.0", @@ -18627,7 +18721,6 @@ "version": "4.4.0", "resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz", "integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==", - "dev": true, "requires": { "min-document": "^2.19.0", "process": "^0.11.10" @@ -20030,6 +20123,11 @@ "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=", "dev": true }, + "individual": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/individual/-/individual-2.0.0.tgz", + "integrity": "sha512-pWt8hBCqJsUWI/HtcfWod7+N9SgAqyPEaF7JQjwzjn5vGrpg6aQ5qeAFQ7dx//UH4J1O+7xqew+gCeeFt6xN/g==" + }, "infer-owner": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", @@ -20545,8 +20643,7 @@ "is-function": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-function/-/is-function-1.0.2.tgz", - "integrity": "sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==", - "dev": true + "integrity": "sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==" }, "is-generator-fn": { "version": "2.1.0", @@ -22371,6 +22468,11 @@ "source-map-support": "^0.5.5" } }, + "keycode": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/keycode/-/keycode-2.2.1.tgz", + "integrity": "sha512-Rdgz9Hl9Iv4QKi8b0OlCRQEzp4AgVxyCtz5S/+VIHezDmrDhkp2N2TqBWOLz0/gbeREXOOiI9/4b8BY9uw2vFg==" + }, "keytar": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.2.0.tgz", @@ -22940,6 +23042,31 @@ "yallist": "^4.0.0" } }, + "m3u8-parser": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/m3u8-parser/-/m3u8-parser-4.8.0.tgz", + "integrity": "sha512-UqA2a/Pw3liR6Df3gwxrqghCP17OpPlQj6RBPLYygf/ZSQ4MoSgvdvhvt35qV+3NaaA0FSZx93Ix+2brT1U7cA==", + "requires": { + "@babel/runtime": "^7.12.5", + "@videojs/vhs-utils": "^3.0.5", + "global": "^4.4.0" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.7.tgz", + "integrity": "sha512-UF0tvkUtxwAgZ5W/KrkHf0Rn0fdnLDU9ScxBrEVNUprE/MzirjK4MJUX1/BVDv00Sv8cljtukVK1aky++X1SjQ==", + "requires": { + "regenerator-runtime": "^0.13.11" + } + }, + "regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" + } + } + }, "macos-release": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-2.5.0.tgz", @@ -23425,7 +23552,6 @@ "version": "2.19.0", "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz", "integrity": "sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==", - "dev": true, "requires": { "dom-walk": "^0.1.0" } @@ -23708,6 +23834,32 @@ } } }, + "mpd-parser": { + "version": "0.22.1", + "resolved": "https://registry.npmjs.org/mpd-parser/-/mpd-parser-0.22.1.tgz", + "integrity": "sha512-fwBebvpyPUU8bOzvhX0VQZgSohncbgYwUyJJoTSNpmy7ccD2ryiCvM7oRkn/xQH5cv73/xU7rJSNCLjdGFor0Q==", + "requires": { + "@babel/runtime": "^7.12.5", + "@videojs/vhs-utils": "^3.0.5", + "@xmldom/xmldom": "^0.8.3", + "global": "^4.4.0" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.7.tgz", + "integrity": "sha512-UF0tvkUtxwAgZ5W/KrkHf0Rn0fdnLDU9ScxBrEVNUprE/MzirjK4MJUX1/BVDv00Sv8cljtukVK1aky++X1SjQ==", + "requires": { + "regenerator-runtime": "^0.13.11" + } + }, + "regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" + } + } + }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -23740,6 +23892,30 @@ "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==" }, + "mux.js": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/mux.js/-/mux.js-6.0.1.tgz", + "integrity": "sha512-22CHb59rH8pWGcPGW5Og7JngJ9s+z4XuSlYvnxhLuc58cA1WqGDQPzuG8I+sPm1/p0CdgpzVTaKW408k5DNn8w==", + "requires": { + "@babel/runtime": "^7.11.2", + "global": "^4.4.0" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.7.tgz", + "integrity": "sha512-UF0tvkUtxwAgZ5W/KrkHf0Rn0fdnLDU9ScxBrEVNUprE/MzirjK4MJUX1/BVDv00Sv8cljtukVK1aky++X1SjQ==", + "requires": { + "regenerator-runtime": "^0.13.11" + } + }, + "regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" + } + } + }, "nan": { "version": "2.14.1", "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.1.tgz", @@ -24875,6 +25051,29 @@ "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", "dev": true }, + "ogv": { + "version": "1.8.9", + "resolved": "https://registry.npmjs.org/ogv/-/ogv-1.8.9.tgz", + "integrity": "sha512-tQA2E3E2PzdWqxIaI5X8q8Vxvj1Ap3JSZmD1MfnA+cTY3o0t+06zY4RKXckQ9pxeqGy/UH4l4QensssmbPLwAQ==", + "requires": { + "@babel/runtime": "^7.16.7" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.7.tgz", + "integrity": "sha512-UF0tvkUtxwAgZ5W/KrkHf0Rn0fdnLDU9ScxBrEVNUprE/MzirjK4MJUX1/BVDv00Sv8cljtukVK1aky++X1SjQ==", + "requires": { + "regenerator-runtime": "^0.13.11" + } + }, + "regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" + } + } + }, "on-finished": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", @@ -25905,6 +26104,14 @@ "node-modules-regexp": "^1.0.0" } }, + "pkcs7": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/pkcs7/-/pkcs7-1.0.4.tgz", + "integrity": "sha512-afRERtHn54AlwaF2/+LFszyAANTCggGilmcmILUzEjvs3XgFZT+xE6+QWQcAGmu4xajy+Xtj7acLOPdx5/eXWQ==", + "requires": { + "@babel/runtime": "^7.5.5" + } + }, "pkg-dir": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", @@ -26858,8 +27065,7 @@ "process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", - "dev": true + "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=" }, "process-nextick-args": { "version": "2.0.1", @@ -28270,8 +28476,7 @@ "regenerator-runtime": { "version": "0.13.5", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz", - "integrity": "sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==", - "dev": true + "integrity": "sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==" }, "regenerator-transform": { "version": "0.14.5", @@ -28944,6 +29149,14 @@ "aproba": "^1.1.1" } }, + "rust-result": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rust-result/-/rust-result-1.0.0.tgz", + "integrity": "sha512-6cJzSBU+J/RJCF063onnQf0cDUOHs9uZI1oroSGnHOph+CQTIJ5Pp2hK5kEQq1+7yE/EEWfulSNXAQ2jikPthA==", + "requires": { + "individual": "^2.0.0" + } + }, "rxjs": { "version": "6.5.5", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.5.tgz", @@ -28964,6 +29177,14 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, + "safe-json-parse": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/safe-json-parse/-/safe-json-parse-4.0.0.tgz", + "integrity": "sha512-RjZPPHugjK0TOzFrLZ8inw44s9bKox99/0AZW9o/BEQVrJfhI+fIHMErnPyRa89/yRXUUr93q+tiN6zhoVV4wQ==", + "requires": { + "rust-result": "^1.0.0" + } + }, "safe-regex": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", @@ -32936,6 +33157,11 @@ "prepend-http": "^2.0.0" } }, + "url-toolkit": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/url-toolkit/-/url-toolkit-2.2.5.tgz", + "integrity": "sha512-mtN6xk+Nac+oyJ/PrI7tzfmomRVNFIWKUbG8jdYFt52hxbiReFAXIjYskvu64/dvuW71IcB7lV8l0HvZMac6Jg==" + }, "use": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", @@ -33118,6 +33344,54 @@ "extsprintf": "^1.2.0" } }, + "video.js": { + "version": "7.21.1", + "resolved": "https://registry.npmjs.org/video.js/-/video.js-7.21.1.tgz", + "integrity": "sha512-AvHfr14ePDHCfW5Lx35BvXk7oIonxF6VGhSxocmTyqotkQpxwYdmt4tnQSV7MYzNrYHb0GI8tJMt20NDkCQrxg==", + "requires": { + "@babel/runtime": "^7.12.5", + "@videojs/http-streaming": "2.15.1", + "@videojs/vhs-utils": "^3.0.4", + "@videojs/xhr": "2.6.0", + "aes-decrypter": "3.1.3", + "global": "^4.4.0", + "keycode": "^2.2.0", + "m3u8-parser": "4.8.0", + "mpd-parser": "0.22.1", + "mux.js": "6.0.1", + "safe-json-parse": "4.0.0", + "videojs-font": "3.2.0", + "videojs-vtt.js": "^0.15.4" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.7.tgz", + "integrity": "sha512-UF0tvkUtxwAgZ5W/KrkHf0Rn0fdnLDU9ScxBrEVNUprE/MzirjK4MJUX1/BVDv00Sv8cljtukVK1aky++X1SjQ==", + "requires": { + "regenerator-runtime": "^0.13.11" + } + }, + "regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" + } + } + }, + "videojs-font": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/videojs-font/-/videojs-font-3.2.0.tgz", + "integrity": "sha512-g8vHMKK2/JGorSfqAZQUmYYNnXmfec4MLhwtEFS+mMs2IDY398GLysy6BH6K+aS1KMNu/xWZ8Sue/X/mdQPliA==" + }, + "videojs-vtt.js": { + "version": "0.15.4", + "resolved": "https://registry.npmjs.org/videojs-vtt.js/-/videojs-vtt.js-0.15.4.tgz", + "integrity": "sha512-r6IhM325fcLb1D6pgsMkTQT1PpFdUdYZa1iqk7wJEu+QlibBwATPfPc9Bg8Jiym0GE5yP1AG2rMLu+QMVWkYtA==", + "requires": { + "global": "^4.3.1" + } + }, "vinyl": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.1.tgz", diff --git a/package.json b/package.json index 61b897323..81bf000bb 100644 --- a/package.json +++ b/package.json @@ -123,9 +123,11 @@ "moment": "2.29.4", "moment-timezone": "0.5.38", "nl.kingsquare.cordova.background-audio": "1.0.1", + "ogv": "1.8.9", "rxjs": "6.5.5", "ts-md5": "1.2.7", "tslib": "2.3.1", + "video.js": "7.21.1", "zone.js": "0.10.3" }, "devDependencies": { diff --git a/scripts/copy-assets.js b/scripts/copy-assets.js index 3b3f03ea4..03fec6038 100644 --- a/scripts/copy-assets.js +++ b/scripts/copy-assets.js @@ -28,6 +28,8 @@ const ASSETS = { '/node_modules/mathjax/jax/output/PreviewHTML': '/lib/mathjax/jax/output/PreviewHTML', '/node_modules/mathjax/localization': '/lib/mathjax/localization', '/src/core/features/h5p/assets': '/lib/h5p', + '/node_modules/ogv/dist': '/lib/ogv', + '/node_modules/video.js/dist/video-js.min.css': '/lib/video.js/video-js.min.css', }; module.exports = function(ctx) { diff --git a/src/addons/filter/mediaplugin/classes/videojs-ogvjs.ts b/src/addons/filter/mediaplugin/classes/videojs-ogvjs.ts new file mode 100644 index 000000000..16fc96e19 --- /dev/null +++ b/src/addons/filter/mediaplugin/classes/videojs-ogvjs.ts @@ -0,0 +1,748 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { CorePlatform } from '@services/platform'; +import { OGVPlayer, OGVCompat, OGVLoader } from 'ogv'; +import videojs, { PreloadOption, TechSourceObject, VideoJSOptions } from 'video.js'; + +export const Tech = videojs.getComponent('Tech'); + +/** + * Object.defineProperty but "lazy", which means that the value is only set after + * it retrieved the first time, rather than being set right away. + * + * @param obj The object to set the property on. + * @param key The key for the property to set. + * @param getValue The function used to get the value when it is needed. + * @param setter Whether a setter should be allowed or not. + * @returns Object. + */ +const defineLazyProperty = (obj: T, key: string, getValue: () => unknown, setter = true): T => { + const set = (value: unknown): void => { + Object.defineProperty(obj, key, { value, enumerable: true, writable: true }); + }; + + const options: PropertyDescriptor = { + configurable: true, + enumerable: true, + get() { + const value = getValue(); + + set(value); + + return value; + }, + }; + + if (setter) { + options.set = set; + } + + return Object.defineProperty(obj, key, options); +}; + +/** + * OgvJS Media Controller for VideoJS - Wrapper for ogv.js Media API. + * + * Code adapted from https://github.com/HuongNV13/videojs-ogvjs/blob/f9b12bd53018d967bb305f02725834a98f20f61f/src/plugin.js + * Modified in the following ways: + * - Adapted to Typescript. + * - Use our own functions to detect the platform instead of using getDeviceOS. + * - Add an initialize static function. + * - In the play function, reset the media if it already ended to fix problems with replaying media. + * - Allow full screen in iOS devices, and implement enterFullScreen and exitFullScreen to use a fake full screen. + */ +export class VideoJSOgvJS extends Tech { + + /** + * List of available events of the media player. + */ + // eslint-disable-next-line @typescript-eslint/naming-convention + static readonly Events = [ + 'loadstart', + 'suspend', + 'abort', + 'error', + 'emptied', + 'stalled', + 'loadedmetadata', + 'loadeddata', + 'canplay', + 'canplaythrough', + 'playing', + 'waiting', + 'seeking', + 'seeked', + 'ended', + 'durationchange', + 'timeupdate', + 'progress', + 'play', + 'pause', + 'ratechange', + 'resize', + 'volumechange', + ]; + + protected playerId?: string; + protected parentElement: HTMLElement | null = null; + protected placeholderElement = document.createElement('div'); + + // Variables/functions defined in parent classes. + protected el_!: OGVPlayerEl; // eslint-disable-line @typescript-eslint/naming-convention + protected options_!: VideoJSOptions; // eslint-disable-line @typescript-eslint/naming-convention + protected currentSource_?: TechSourceObject; // eslint-disable-line @typescript-eslint/naming-convention + protected triggerReady!: () => void; + protected on!: (name: string, callback: (e?: Event) => void) => void; + + /** + * Create an instance of this Tech. + * + * @param options The key/value store of player options. + * @param ready Callback function to call when the `OgvJS` Tech is ready. + */ + constructor(options: VideoJSTechOptions, ready: () => void) { + super(options, ready); + + this.el_.src = options.src || options.source?.src || options.sources?.[0]?.src || this.el_.src; + VideoJSOgvJS.setIfAvailable(this.el_, 'autoplay', options.autoplay); + VideoJSOgvJS.setIfAvailable(this.el_, 'loop', options.loop); + VideoJSOgvJS.setIfAvailable(this.el_, 'poster', options.poster); + VideoJSOgvJS.setIfAvailable(this.el_, 'preload', options.preload); + this.playerId = options.playerId; + + this.on('loadedmetadata', () => { + if (CorePlatform.isIPhone()) { + // iPhoneOS add some inline styles to the canvas, we need to remove it. + const canvas = this.el_.getElementsByTagName('canvas')[0]; + + canvas.style.removeProperty('width'); + canvas.style.removeProperty('margin'); + } + + this.triggerReady(); + }); + } + + /** + * Set the value for the player is it has that property. + * + * @param el HTML player. + * @param name Name of the property. + * @param value Value to set. + */ + static setIfAvailable(el: HTMLElement, name: string, value: unknown): void { + // eslint-disable-next-line no-prototype-builtins + if (el.hasOwnProperty(name)) { + el[name] = value; + } + }; + + /** + * Check if browser/device is supported by Ogv.JS. + * + * @returns Whether it's supported. + */ + static isSupported(): boolean { + return OGVCompat.supported('OGVPlayer'); + }; + + /** + * Check if the tech can support the given type. + * + * @param type The mimetype to check. + * @returns 'probably', 'maybe', or '' (empty string). + */ + static canPlayType(type: string): string { + return (type.indexOf('/ogg') !== -1 || type.indexOf('/webm')) ? 'maybe' : ''; + }; + + /** + * Check if the tech can support the given source. + * + * @param srcObj The source object. + * @returns The options passed to the tech. + */ + static canPlaySource(srcObj: TechSourceObject): string { + return VideoJSOgvJS.canPlayType(srcObj.type); + }; + + /** + * Check if the volume can be changed in this browser/device. + * Volume cannot be changed in a lot of mobile devices. + * Specifically, it can't be changed from 1 on iOS. + * + * @returns True if volume can be controlled. + */ + static canControlVolume(): boolean { + if (CorePlatform.isIPhone()) { + return false; + } + + const player = new OGVPlayer(); + + // eslint-disable-next-line no-prototype-builtins + return player.hasOwnProperty('volume'); + }; + + /** + * Check if the volume can be muted in this browser/device. + * + * @returns True if volume can be muted. + */ + static canMuteVolume(): boolean { + return true; + }; + + /** + * Check if the playback rate can be changed in this browser/device. + * + * @returns True if playback rate can be controlled. + */ + static canControlPlaybackRate(): boolean { + return true; + }; + + /** + * Check to see if native 'TextTracks' are supported by this browser/device. + * + * @returns True if native 'TextTracks' are supported. + */ + static supportsNativeTextTracks(): boolean { + return false; + }; + + /** + * Check if the fullscreen resize is supported by this browser/device. + * + * @returns True if the fullscreen resize is supported. + */ + static supportsFullscreenResize(): boolean { + return true; + }; + + /** + * Check if the progress events is supported by this browser/device. + * + * @returns True if the progress events is supported. + */ + static supportsProgressEvents(): boolean { + return true; + }; + + /** + * Check if the time update events is supported by this browser/device. + * + * @returns True if the time update events is supported. + */ + static supportsTimeupdateEvents(): boolean { + return true; + }; + + /** + * Create the 'OgvJS' Tech's DOM element. + * + * @returns The element that gets created. + */ + createEl(): OGVPlayerEl { + const options = this.options_; + + if (options.base) { + OGVLoader.base = options.base; + } else if (!OGVLoader.base) { + throw new Error('Please specify the base for the ogv.js library'); + } + + const el = new OGVPlayer(options); + + el.className += ' vjs-tech'; + options.tag = el; + + return el; + } + + /** + * Start playback. + */ + play(): void { + if (this.ended()) { + // Reset the player, otherwise the Replay button doesn't work. + this.el_.stop(); + } + + this.el_.play(); + } + + /** + * Get the current playback speed. + * + * @returns Playback speed. + */ + playbackRate(): number { + return this.el_.playbackRate || 1; + } + + /** + * Set the playback speed. + * + * @param val Speed for the player to play. + */ + setPlaybackRate(val: number): void { + // eslint-disable-next-line no-prototype-builtins + if (this.el_.hasOwnProperty('playbackRate')) { + this.el_.playbackRate = val; + } + } + + /** + * Returns a TimeRanges object that represents the ranges of the media resource that the user agent has played. + * + * @returns The range of points on the media timeline that has been reached through normal playback. + */ + played(): TimeRanges { + return this.el_.played; + } + + /** + * Pause playback. + */ + pause(): void { + this.el_.pause(); + } + + /** + * Is the player paused or not. + * + * @returns Whether is paused. + */ + paused(): boolean { + return this.el_.paused; + } + + /** + * Get current playing time. + * + * @returns Current time. + */ + currentTime(): number { + return this.el_.currentTime; + } + + /** + * Set current playing time. + * + * @param seconds Current time of audio/video. + */ + setCurrentTime(seconds: number): void { + try { + this.el_.currentTime = seconds; + } catch (e) { + videojs.log(e, 'Media is not ready. (Video.JS)'); + } + } + + /** + * Get media's duration. + * + * @returns Duration. + */ + duration(): number { + if (this.el_.duration && this.el_.duration !== Infinity) { + return this.el_.duration; + } + + return 0; + } + + /** + * Get a TimeRange object that represents the intersection + * of the time ranges for which the user agent has all + * relevant media. + * + * @returns Time ranges. + */ + buffered(): TimeRanges { + return this.el_.buffered; + } + + /** + * Get current volume level. + * + * @returns Volume. + */ + volume(): number { + // eslint-disable-next-line no-prototype-builtins + return this.el_.hasOwnProperty('volume') ? this.el_.volume : 1; + } + + /** + * Set current playing volume level. + * + * @param percentAsDecimal Volume percent as a decimal. + */ + setVolume(percentAsDecimal: number): void { + // eslint-disable-next-line no-prototype-builtins + if (!CorePlatform.isIPhone() && this.el_.hasOwnProperty('volume')) { + this.el_.volume = percentAsDecimal; + } + } + + /** + * Is the player muted or not. + * + * @returns Whether it's muted. + */ + muted(): boolean { + return this.el_.muted; + } + + /** + * Mute the player. + * + * @param muted True to mute the player. + */ + setMuted(muted: boolean): void { + this.el_.muted = !!muted; + } + + /** + * Is the player muted by default or not. + * + * @returns Whether it's muted by default. + */ + defaultMuted(): boolean { + return this.el_.defaultMuted || false; + } + + /** + * Get the player width. + * + * @returns Width. + */ + width(): number { + return this.el_.offsetWidth; + } + + /** + * Get the player height. + * + * @returns Height. + */ + height(): number { + return this.el_.offsetHeight; + } + + /** + * Get the video width. + * + * @returns Video width. + */ + videoWidth(): number { + return ( this.el_).videoWidth ?? 0; + } + + /** + * Get the video height. + * + * @returns Video heigth. + */ + videoHeight(): number { + return ( this.el_).videoHeight ?? 0; + } + + /** + * Get/set media source. + * + * @param src Source. + * @returns Source when getting it, undefined when setting it. + */ + src(src?: string): string | undefined { + if (typeof src === 'undefined') { + return this.el_.src; + } + + this.el_.src = src; + } + + /** + * Load the media into the player. + */ + load(): void { + this.el_.load(); + } + + /** + * Get current media source. + * + * @returns Current source. + */ + currentSrc(): string { + if (this.currentSource_) { + return this.currentSource_.src; + } + + return this.el_.currentSrc; + } + + /** + * Get media poster URL. + * + * @returns Poster. + */ + poster(): string { + return 'poster' in this.el_ ? this.el_.poster : ''; + } + + /** + * Set media poster URL. + * + * @param url The poster image's url. + */ + setPoster(url: string): void { + ( this.el_).poster = url; + } + + /** + * Is the media preloaded or not. + * + * @returns Whether it's preloaded. + */ + preload(): PreloadOption { + return this.el_.preload || 'none'; + } + + /** + * Set the media preload method. + * + * @param val Value for preload attribute. + */ + setPreload(val: PreloadOption): void { + // eslint-disable-next-line no-prototype-builtins + if (this.el_.hasOwnProperty('preload')) { + this.el_.preload = val; + } + } + + /** + * Is the media auto-played or not. + * + * @returns Whether it's auto-played. + */ + autoplay(): boolean { + return this.el_.autoplay || false; + } + + /** + * Set media autoplay method. + * + * @param val Value for autoplay attribute. + */ + setAutoplay(val: boolean): void { + // eslint-disable-next-line no-prototype-builtins + if (this.el_.hasOwnProperty('autoplay')) { + this.el_.autoplay = !!val; + } + } + + /** + * Does the media has controls or not. + * + * @returns Whether it has controls. + */ + controls(): boolean { + return this.el_.controls || false; + } + + /** + * Set the media controls method. + * + * @param val Value for controls attribute. + */ + setControls(val: boolean): void { + // eslint-disable-next-line no-prototype-builtins + if (this.el_.hasOwnProperty('controls')) { + this.el_.controls = !!val; + } + } + + /** + * Is the media looped or not. + * + * @returns Whether it's looped. + */ + loop(): boolean { + return this.el_.loop || false; + } + + /** + * Set the media loop method. + * + * @param val Value for loop attribute. + */ + setLoop(val: boolean): void { + // eslint-disable-next-line no-prototype-builtins + if (this.el_.hasOwnProperty('loop')) { + this.el_.loop = !!val; + } + } + + /** + * Get a TimeRanges object that represents the + * ranges of the media resource to which it is possible + * for the user agent to seek. + * + * @returns Time ranges. + */ + seekable(): TimeRanges { + return this.el_.seekable; + } + + /** + * Is player in the "seeking" state or not. + * + * @returns Whether is in the seeking state. + */ + seeking(): boolean { + return this.el_.seeking; + } + + /** + * Is the media ended or not. + * + * @returns Whether it's ended. + */ + ended(): boolean { + return this.el_.ended; + } + + /** + * Get the current state of network activity + * NETWORK_EMPTY (numeric value 0) + * NETWORK_IDLE (numeric value 1) + * NETWORK_LOADING (numeric value 2) + * NETWORK_NO_SOURCE (numeric value 3) + * + * @returns Network state. + */ + networkState(): number { + return this.el_.networkState; + } + + /** + * Get the current state of the player. + * HAVE_NOTHING (numeric value 0) + * HAVE_METADATA (numeric value 1) + * HAVE_CURRENT_DATA (numeric value 2) + * HAVE_FUTURE_DATA (numeric value 3) + * HAVE_ENOUGH_DATA (numeric value 4) + * + * @returns Ready state. + */ + readyState(): number { + return this.el_.readyState; + } + + /** + * Does the player support native fullscreen mode or not. (Mobile devices) + * + * @returns Whether it supports full screen. + */ + supportsFullScreen(): boolean { + return !!this.playerId; + } + + /** + * Get media player error. + * + * @returns Error. + */ + error(): MediaError | null { + return this.el_.error; + } + + /** + * Enter full screen mode. + */ + enterFullScreen(): void { + // Use a "fake" full screen mode, moving the player to a different place in DOM to be able to use full screen size. + const player = videojs.getPlayer(this.playerId ?? ''); + if (!player) { + return; + } + + const container = player.el(); + this.parentElement = container.parentElement; + if (!this.parentElement) { + // Shouldn't happen, it means the element is not in DOM. Do not support full screen in this case. + return; + } + + this.parentElement.replaceChild(this.placeholderElement, container); + document.body.appendChild(container); + container.classList.add('vjs-ios-moodleapp-fs'); + + player.isFullscreen(true); + } + + /** + * Exit full screen mode. + */ + exitFullScreen(): void { + if (!this.parentElement) { + return; + } + + const player = videojs.getPlayer(this.playerId ?? ''); + if (!player) { + return; + } + + const container = player.el(); + this.parentElement.replaceChild(container, this.placeholderElement); + container.classList.remove('vjs-ios-moodleapp-fs'); + + player.isFullscreen(false); + } + +} + +[ + ['featuresVolumeControl', 'canControlVolume'], + ['featuresMuteControl', 'canMuteVolume'], + ['featuresPlaybackRate', 'canControlPlaybackRate'], + ['featuresNativeTextTracks', 'supportsNativeTextTracks'], + ['featuresFullscreenResize', 'supportsFullscreenResize'], + ['featuresProgressEvents', 'supportsProgressEvents'], + ['featuresTimeupdateEvents', 'supportsTimeupdateEvents'], +].forEach(([key, fn]) => { + defineLazyProperty(VideoJSOgvJS.prototype, key, () => VideoJSOgvJS[fn](), true); +}); +/** + * Initialize the controller. + */ +export const initializeVideoJSOgvJS = (): void => { + OGVLoader.base = 'assets/lib/ogv'; + Tech.registerTech('OgvJS', VideoJSOgvJS); +}; + +type OGVPlayerEl = (HTMLAudioElement | HTMLVideoElement) & { + stop: () => void; +}; + +/** + * VideoJS Tech options. It includes some options added by VideoJS internally. + */ +type VideoJSTechOptions = VideoJSOptions & { + playerId?: string; +}; diff --git a/src/addons/filter/mediaplugin/mediaplugin.module.ts b/src/addons/filter/mediaplugin/mediaplugin.module.ts index 1977bd08d..821b4db73 100644 --- a/src/addons/filter/mediaplugin/mediaplugin.module.ts +++ b/src/addons/filter/mediaplugin/mediaplugin.module.ts @@ -15,6 +15,7 @@ import { APP_INITIALIZER, NgModule } from '@angular/core'; import { CoreFilterDelegate } from '@features/filter/services/filter-delegate'; +import { initializeVideoJSOgvJS } from './classes/videojs-ogvjs'; import { AddonFilterMediaPluginHandler } from './services/handlers/mediaplugin'; @NgModule({ @@ -26,7 +27,11 @@ import { AddonFilterMediaPluginHandler } from './services/handlers/mediaplugin'; { provide: APP_INITIALIZER, multi: true, - useValue: () => CoreFilterDelegate.registerHandler(AddonFilterMediaPluginHandler.instance), + useValue: () => { + CoreFilterDelegate.registerHandler(AddonFilterMediaPluginHandler.instance); + + initializeVideoJSOgvJS(); + }, }, ], }) diff --git a/src/addons/filter/mediaplugin/services/handlers/mediaplugin.ts b/src/addons/filter/mediaplugin/services/handlers/mediaplugin.ts index 509451927..503233620 100644 --- a/src/addons/filter/mediaplugin/services/handlers/mediaplugin.ts +++ b/src/addons/filter/mediaplugin/services/handlers/mediaplugin.ts @@ -13,11 +13,17 @@ // limitations under the License. import { Injectable } from '@angular/core'; +import { CoreExternalContentDirective } from '@directives/external-content'; import { CoreFilterDefaultHandler } from '@features/filter/services/handlers/default-filter'; +import { CoreLang } from '@services/lang'; import { CoreTextUtils } from '@services/utils/text'; import { CoreUrlUtils } from '@services/utils/url'; import { makeSingleton } from '@singletons'; +import { CoreDirectivesRegistry } from '@singletons/directives-registry'; +import { CoreEvents } from '@singletons/events'; +import { CoreMedia } from '@singletons/media'; +import videojs, { VideoJSOptions, VideoJSPlayer } from 'video.js'; /** * Handler to support the Multimedia filter. @@ -41,25 +47,100 @@ export class AddonFilterMediaPluginHandlerService extends CoreFilterDefaultHandl const videos = Array.from(this.template.content.querySelectorAll('video')); videos.forEach((video) => { - this.treatVideoFilters(video); + this.treatYoutubeVideos(video); }); return this.template.innerHTML; } /** - * Treat video filters. Currently only treating youtube video using video JS. + * @inheritdoc + */ + handleHtml(container: HTMLElement): void { + const mediaElements = Array.from(container.querySelectorAll('video, audio')); + + mediaElements.forEach((mediaElement) => { + if (CoreMedia.mediaUsesJavascriptPlayer(mediaElement)) { + this.useVideoJS(mediaElement); + } else { + // Remove the VideoJS classes and data if present. + mediaElement.classList.remove('video-js'); + mediaElement.removeAttribute('data-setup'); + mediaElement.removeAttribute('data-setup-lazy'); + } + }); + } + + /** + * Use video JS in a certain video or audio. + * + * @param mediaElement Media element. + */ + protected async useVideoJS(mediaElement: HTMLVideoElement | HTMLAudioElement): Promise { + const lang = await CoreLang.getCurrentLanguage(); + + // Wait for external-content to finish in the element and its sources. + await Promise.all([ + CoreDirectivesRegistry.waitDirectivesReady(mediaElement, undefined, CoreExternalContentDirective), + CoreDirectivesRegistry.waitDirectivesReady(mediaElement, 'source', CoreExternalContentDirective), + ]); + + const dataSetupString = mediaElement.getAttribute('data-setup') || mediaElement.getAttribute('data-setup-lazy') || '{}'; + const data = CoreTextUtils.parseJSON(dataSetupString, {}); + + const player = videojs(mediaElement, { + controls: true, + techOrder: ['OgvJS'], + language: lang, + controlBar: { + pictureInPictureToggle: false, + }, + aspectRatio: data.aspectRatio, + }, () => { + if (mediaElement.tagName === 'VIDEO') { + this.fixVideoJSPlayerSize(player); + } + }); + + CoreEvents.trigger(CoreEvents.JS_PLAYER_CREATED, { + id: mediaElement.id, + element: mediaElement, + player, + }); + } + + /** + * Fix VideoJS player size. + * If video width is wider than available width, video is cut off. Fix the dimensions in this case. + * + * @param player Player instance. + */ + protected fixVideoJSPlayerSize(player: VideoJSPlayer): void { + const videoWidth = player.videoWidth(); + const videoHeight = player.videoHeight(); + const playerDimensions = player.currentDimensions(); + if (!videoWidth || !videoHeight || !playerDimensions.width || videoWidth === playerDimensions.width) { + return; + } + + const candidateHeight = playerDimensions.width * videoHeight / videoWidth; + if (!playerDimensions.height || Math.abs(candidateHeight - playerDimensions.height) > 1) { + player.dimension('height', candidateHeight); + } + } + + /** + * Treat Video JS Youtube video links and translate them to iframes. * * @param video Video element. */ - protected treatVideoFilters(video: HTMLElement): void { - // Treat Video JS Youtube video links and translate them to iframes. + protected treatYoutubeVideos(video: HTMLElement): void { if (!video.classList.contains('video-js')) { return; } const dataSetupString = video.getAttribute('data-setup') || video.getAttribute('data-setup-lazy') || '{}'; - const data = CoreTextUtils.parseJSON(dataSetupString, {}); + const data = CoreTextUtils.parseJSON(dataSetupString, {}); const youtubeUrl = data.techOrder?.[0] == 'youtube' && CoreUrlUtils.getYoutubeEmbedUrl(data.sources?.[0]?.src); if (!youtubeUrl) { @@ -81,10 +162,3 @@ export class AddonFilterMediaPluginHandlerService extends CoreFilterDefaultHandl } export const AddonFilterMediaPluginHandler = makeSingleton(AddonFilterMediaPluginHandlerService); - -type VideoDataSetup = { - techOrder?: string[]; - sources?: { - src?: string; - }[]; -}; diff --git a/src/addons/mod/data/fields/latlong/component/latlong.ts b/src/addons/mod/data/fields/latlong/component/latlong.ts index 38398beae..17c7a4f18 100644 --- a/src/addons/mod/data/fields/latlong/component/latlong.ts +++ b/src/addons/mod/data/fields/latlong/component/latlong.ts @@ -18,8 +18,8 @@ import { Component } from '@angular/core'; import { FormBuilder } from '@angular/forms'; import { SafeUrl } from '@angular/platform-browser'; import { CoreAnyError } from '@classes/errors/error'; -import { CoreApp } from '@services/app'; import { CoreGeolocation, CoreGeolocationError, CoreGeolocationErrorReason } from '@services/geolocation'; +import { CorePlatform } from '@services/platform'; import { CoreDomUtils } from '@services/utils/dom'; import { DomSanitizer } from '@singletons'; @@ -73,7 +73,7 @@ export class AddonModDataFieldLatlongComponent extends AddonModDataFieldPluginBa const northFixed = north ? north.toFixed(4) : '0.0000'; const eastFixed = east ? east.toFixed(4) : '0.0000'; - if (CoreApp.isIOS()) { + if (CorePlatform.isIOS()) { url = 'http://maps.apple.com/?ll=' + northFixed + ',' + eastFixed + '&near=' + northFixed + ',' + eastFixed; } else { url = 'geo:' + northFixed + ',' + eastFixed; diff --git a/src/addons/mod/quiz/pages/player/player.page.ts b/src/addons/mod/quiz/pages/player/player.page.ts index 4eb7dd7ea..42b91c462 100644 --- a/src/addons/mod/quiz/pages/player/player.page.ts +++ b/src/addons/mod/quiz/pages/player/player.page.ts @@ -47,7 +47,7 @@ import { CanLeave } from '@guards/can-leave'; import { CoreForms } from '@singletons/form'; import { CoreDom } from '@singletons/dom'; import { CoreTime } from '@singletons/time'; -import { CoreComponentsRegistry } from '@singletons/components-registry'; +import { CoreDirectivesRegistry } from '@singletons/directives-registry'; /** * Page that allows attempting a quiz. @@ -690,7 +690,7 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave { */ protected async scrollToQuestion(slot: number): Promise { await CoreUtils.nextTick(); - await CoreComponentsRegistry.waitComponentsReady(this.elementRef.nativeElement, 'core-question'); + await CoreDirectivesRegistry.waitDirectivesReady(this.elementRef.nativeElement, 'core-question'); await CoreDom.scrollToElement( this.elementRef.nativeElement, '#addon-mod_quiz-question-' + slot, diff --git a/src/addons/mod/resource/components/index/index.ts b/src/addons/mod/resource/components/index/index.ts index 545082efc..1fb0ecfeb 100644 --- a/src/addons/mod/resource/components/index/index.ts +++ b/src/addons/mod/resource/components/index/index.ts @@ -19,7 +19,6 @@ import { CoreCourseModuleMainResourceComponent } from '@features/course/classes/ import { CoreCourseContentsPage } from '@features/course/pages/contents/contents'; import { CoreCourse } from '@features/course/services/course'; import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate'; -import { CoreApp } from '@services/app'; import { CoreNetwork } from '@services/network'; import { CoreFileHelper } from '@services/file-helper'; import { CoreSites } from '@services/sites'; @@ -35,6 +34,7 @@ import { AddonModResourceProvider, } from '../../services/resource'; import { AddonModResourceHelper } from '../../services/resource-helper'; +import { CorePlatform } from '@services/platform'; /** * Component that displays a resource. @@ -79,7 +79,7 @@ export class AddonModResourceIndexComponent extends CoreCourseModuleMainResource async ngOnInit(): Promise { super.ngOnInit(); - this.isIOS = CoreApp.isIOS(); + this.isIOS = CorePlatform.isIOS(); this.isOnline = CoreNetwork.isOnline(); // Refresh online status when changes. diff --git a/src/addons/qtype/ddwtos/classes/ddwtos.ts b/src/addons/qtype/ddwtos/classes/ddwtos.ts index 152b38766..a06ac2928 100644 --- a/src/addons/qtype/ddwtos/classes/ddwtos.ts +++ b/src/addons/qtype/ddwtos/classes/ddwtos.ts @@ -15,7 +15,7 @@ import { CoreFormatTextDirective } from '@directives/format-text'; import { CoreTextUtils } from '@services/utils/text'; import { CoreUtils } from '@services/utils/utils'; -import { CoreComponentsRegistry } from '@singletons/components-registry'; +import { CoreDirectivesRegistry } from '@singletons/directives-registry'; import { CoreCoordinates, CoreDom } from '@singletons/dom'; import { CoreEventObserver } from '@singletons/events'; import { CoreLogger } from '@singletons/logger'; @@ -427,7 +427,7 @@ export class AddonQtypeDdwtosQuestion { protected async waitForReady(): Promise { await CoreDom.waitToBeInDOM(this.container); - await CoreComponentsRegistry.waitComponentsReady(this.container, 'core-format-text', CoreFormatTextDirective); + await CoreDirectivesRegistry.waitDirectivesReady(this.container, 'core-format-text', CoreFormatTextDirective); const drag = Array.from(this.container.querySelectorAll(this.selectors.dragHomes()))[0]; diff --git a/src/core/classes/async-component.ts b/src/core/classes/async-directive.ts similarity index 80% rename from src/core/classes/async-component.ts rename to src/core/classes/async-directive.ts index 77635a70f..b659dc253 100644 --- a/src/core/classes/async-component.ts +++ b/src/core/classes/async-directive.ts @@ -13,12 +13,12 @@ // limitations under the License. /** - * Component that is not rendered immediately after being mounted. + * Directive that is not rendered immediately after being mounted. */ -export interface AsyncComponent { +export interface AsyncDirective { /** - * Wait until the component is fully rendered and ready. + * Wait until the directive is fully rendered and ready. */ ready(): Promise; } diff --git a/src/core/classes/element-controllers/ElementController.ts b/src/core/classes/element-controllers/ElementController.ts index e4ef1b000..d7e889e58 100644 --- a/src/core/classes/element-controllers/ElementController.ts +++ b/src/core/classes/element-controllers/ElementController.ts @@ -18,6 +18,7 @@ export abstract class ElementController { protected enabled: boolean; + protected destroyed = false; constructor(enabled: boolean) { this.enabled = enabled; @@ -49,6 +50,19 @@ export abstract class ElementController { this.onDisabled(); } + /** + * Destroy the element. + */ + destroy(): void { + if (this.destroyed) { + return; + } + + this.destroyed = true; + + this.onDestroy(); + } + /** * Update underlying element to enable interactivity. */ @@ -59,4 +73,11 @@ export abstract class ElementController { */ abstract onDisabled(): void; + /** + * Destroy/dispose pertinent data. + */ + onDestroy(): void { + // By default, nothing to destroy. + } + } diff --git a/src/core/classes/element-controllers/MediaElementController.ts b/src/core/classes/element-controllers/MediaElementController.ts index 0ae911fc0..247c3985e 100644 --- a/src/core/classes/element-controllers/MediaElementController.ts +++ b/src/core/classes/element-controllers/MediaElementController.ts @@ -14,6 +14,10 @@ import { CoreUtils } from '@services/utils/utils'; import { ElementController } from './ElementController'; +import videojs, { VideoJSPlayer } from 'video.js'; +import { CorePromisedValue } from '@classes/promised-value'; +import { CoreEventObserver, CoreEvents } from '@singletons/events'; +import { CoreMedia } from '@singletons/media'; /** * Wrapper class to control the interactivity of a media element. @@ -25,6 +29,10 @@ export class MediaElementController extends ElementController { private playing?: boolean; private playListener?: () => void; private pauseListener?: () => void; + private jsPlayer = new CorePromisedValue(); + private jsPlayerListener?: CoreEventObserver; + private shouldEnable = false; + private shouldDisable = false; constructor(media: HTMLMediaElement, enabled: boolean) { super(enabled); @@ -34,48 +42,119 @@ export class MediaElementController extends ElementController { media.autoplay = false; + if (CoreMedia.mediaUsesJavascriptPlayer(media)) { + const player = this.searchJSPlayer(); + if (player) { + this.jsPlayer.resolve(player); + } else { + this.jsPlayerListener = CoreEvents.on(CoreEvents.JS_PLAYER_CREATED, data => { + if (data.element === media) { + this.jsPlayerListener?.off(); + this.jsPlayer.resolve(data.player); + } + }); + } + } else { + this.jsPlayer.resolve(null); + } + enabled && this.onEnabled(); } /** * @inheritdoc */ - onEnabled(): void { + async onEnabled(): Promise { + this.shouldEnable = true; + this.shouldDisable = false; + + const jsPlayer = await this.jsPlayer; + + if (!this.shouldEnable || this.destroyed) { + return; + } + const ready = this.playing ?? this.autoplay - ? this.media.play() + ? (jsPlayer ?? this.media).play() : Promise.resolve(); - ready - .then(() => this.addPlaybackEventListeners()) - .catch(error => CoreUtils.logUnhandledError('Error enabling media element', error)); + try { + await ready; + + this.addPlaybackEventListeners(jsPlayer); + } catch (error) { + CoreUtils.logUnhandledError('Error enabling media element', error); + } } /** * @inheritdoc */ async onDisabled(): Promise { - this.removePlaybackEventListeners(); + this.shouldDisable = true; + this.shouldEnable = false; - this.media.pause(); + const jsPlayer = await this.jsPlayer; + + if (!this.shouldDisable || this.destroyed) { + return; + } + + this.removePlaybackEventListeners(jsPlayer); + + (jsPlayer ?? this.media).pause(); + } + + /** + * @inheritdoc + */ + async onDestroy(): Promise { + const jsPlayer = await this.jsPlayer; + + this.removePlaybackEventListeners(jsPlayer); + jsPlayer?.dispose(); } /** * Start listening playback events. + * + * @param jsPlayer Javascript player instance (if any). */ - private addPlaybackEventListeners(): void { - this.media.addEventListener('play', this.playListener = () => this.playing = true); - this.media.addEventListener('pause', this.pauseListener = () => this.playing = false); + private addPlaybackEventListeners(jsPlayer: VideoJSPlayer | null): void { + if (jsPlayer) { + jsPlayer.on('play', this.playListener = () => this.playing = true); + jsPlayer.on('pause', this.pauseListener = () => this.playing = false); + } else { + this.media.addEventListener('play', this.playListener = () => this.playing = true); + this.media.addEventListener('pause', this.pauseListener = () => this.playing = false); + } } /** * Stop listening playback events. + * + * @param jsPlayer Javascript player instance (if any). */ - private removePlaybackEventListeners(): void { - this.playListener && this.media.removeEventListener('play', this.playListener); - this.pauseListener && this.media.removeEventListener('pause', this.pauseListener); + private removePlaybackEventListeners(jsPlayer: VideoJSPlayer | null): void { + if (jsPlayer) { + this.playListener && jsPlayer.off('play', this.playListener); + this.pauseListener && jsPlayer.off('pause', this.pauseListener); + } else { + this.playListener && this.media.removeEventListener('play', this.playListener); + this.pauseListener && this.media.removeEventListener('pause', this.pauseListener); + } delete this.playListener; delete this.pauseListener; } + /** + * Search JS player instance. + * + * @returns Player instance if found. + */ + private searchJSPlayer(): VideoJSPlayer | null { + return videojs.getPlayer(this.media.id) || videojs.getPlayer(this.media.id.replace('_html5_api', '')); + } + } diff --git a/src/core/classes/page-load-watcher.ts b/src/core/classes/page-load-watcher.ts index 52bba8de7..e062e3b98 100644 --- a/src/core/classes/page-load-watcher.ts +++ b/src/core/classes/page-load-watcher.ts @@ -15,7 +15,7 @@ import { CoreSitesReadingStrategy } from '@services/sites'; import { CoreUtils } from '@services/utils/utils'; import { Subscription } from 'rxjs'; -import { AsyncComponent } from './async-component'; +import { AsyncDirective } from './async-directive'; import { PageLoadsManager } from './page-loads-manager'; import { CorePromisedValue } from './promised-value'; import { WSObservable } from './site'; @@ -27,7 +27,7 @@ export class PageLoadWatcher { protected hasChanges = false; protected ongoingRequests = 0; - protected components = new Set(); + protected components = new Set(); protected loadedTimeout?: number; protected hasChangesPromises: Promise[] = []; @@ -66,7 +66,7 @@ export class PageLoadWatcher { * * @param component Component instance. */ - async watchComponent(component: AsyncComponent): Promise { + async watchComponent(component: AsyncDirective): Promise { this.components.add(component); clearTimeout(this.loadedTimeout); diff --git a/src/core/classes/page-loads-manager.ts b/src/core/classes/page-loads-manager.ts index 24a28c4a1..c3c179ec0 100644 --- a/src/core/classes/page-loads-manager.ts +++ b/src/core/classes/page-loads-manager.ts @@ -16,7 +16,7 @@ import { CoreRefreshButtonModalComponent } from '@components/refresh-button-moda import { CoreNavigator } from '@services/navigator'; import { CoreDomUtils } from '@services/utils/dom'; import { Subject } from 'rxjs'; -import { AsyncComponent } from './async-component'; +import { AsyncDirective } from './async-directive'; import { PageLoadWatcher } from './page-load-watcher'; /** @@ -37,7 +37,7 @@ export class PageLoadsManager { * @param staleWhileRevalidate Whether to use stale while revalidate strategy. * @returns Load watcher to use. */ - startPageLoad(page: AsyncComponent, staleWhileRevalidate: boolean): PageLoadWatcher { + startPageLoad(page: AsyncDirective, staleWhileRevalidate: boolean): PageLoadWatcher { this.initialPath = this.initialPath ?? CoreNavigator.getCurrentPath(); this.currentLoadWatcher = new PageLoadWatcher(this, staleWhileRevalidate); this.ongoingLoadWatchers.add(this.currentLoadWatcher); @@ -53,7 +53,7 @@ export class PageLoadsManager { * @param component Component instance. * @returns Load watcher to use. */ - startComponentLoad(component: AsyncComponent): PageLoadWatcher { + startComponentLoad(component: AsyncDirective): PageLoadWatcher { // If a component is loading data without the page loading data, probably the component is reloading/refreshing. // In that case, create a load watcher instance but don't store it in currentLoadWatcher because it's not a page load. const loadWatcher = this.currentLoadWatcher ?? new PageLoadWatcher(this, false); diff --git a/src/core/classes/tabs.ts b/src/core/classes/tabs.ts index 5fe244c3a..f74babe68 100644 --- a/src/core/classes/tabs.ts +++ b/src/core/classes/tabs.ts @@ -37,8 +37,8 @@ import { CoreDom } from '@singletons/dom'; import { CoreUtils } from '@services/utils/utils'; import { CoreError } from './errors/error'; import { CorePromisedValue } from './promised-value'; -import { AsyncComponent } from './async-component'; -import { CoreComponentsRegistry } from '@singletons/components-registry'; +import { AsyncDirective } from './async-directive'; +import { CoreDirectivesRegistry } from '@singletons/directives-registry'; import { CorePlatform } from '@services/platform'; /** @@ -47,7 +47,7 @@ import { CorePlatform } from '@services/platform'; @Component({ template: '', }) -export class CoreTabsBaseComponent implements OnInit, AfterViewInit, OnChanges, OnDestroy, AsyncComponent { +export class CoreTabsBaseComponent implements OnInit, AfterViewInit, OnChanges, OnDestroy, AsyncDirective { // Minimum tab's width. protected static readonly MIN_TAB_WIDTH = 107; @@ -99,7 +99,7 @@ export class CoreTabsBaseComponent implements OnInit, Aft this.tabAction = new CoreTabsRoleTab(this); - CoreComponentsRegistry.register(element.nativeElement, this); + CoreDirectivesRegistry.register(element.nativeElement, this); } /** diff --git a/src/core/components/context-menu/context-menu.ts b/src/core/components/context-menu/context-menu.ts index 157407562..33710b712 100644 --- a/src/core/components/context-menu/context-menu.ts +++ b/src/core/components/context-menu/context-menu.ts @@ -20,7 +20,7 @@ import { CoreUtils } from '@services/utils/utils'; import { Translate } from '@singletons'; import { CoreContextMenuItemComponent } from './context-menu-item'; import { CoreContextMenuPopoverComponent } from './context-menu-popover'; -import { CoreComponentsRegistry } from '@singletons/components-registry'; +import { CoreDirectivesRegistry } from '@singletons/directives-registry'; /** * This component adds a button (usually in the navigation bar) that displays a context menu popover. @@ -61,7 +61,7 @@ export class CoreContextMenuComponent implements OnInit, OnDestroy { // Calculate the unique ID. this.uniqueId = 'core-context-menu-' + CoreUtils.getUniqueId('CoreContextMenuComponent'); - CoreComponentsRegistry.register(elementRef.nativeElement, this); + CoreDirectivesRegistry.register(elementRef.nativeElement, this); } /** diff --git a/src/core/components/file/file.ts b/src/core/components/file/file.ts index 12f2d2fb2..d15e5e9e4 100644 --- a/src/core/components/file/file.ts +++ b/src/core/components/file/file.ts @@ -13,7 +13,6 @@ // limitations under the License. import { Component, Input, Output, OnInit, OnDestroy, EventEmitter } from '@angular/core'; -import { CoreApp } from '@services/app'; import { CoreNetwork } from '@services/network'; import { CoreFilepool } from '@services/filepool'; import { CoreFileHelper } from '@services/file-helper'; @@ -27,6 +26,7 @@ import { CoreTextUtils } from '@services/utils/text'; import { CoreConstants } from '@/core/constants'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { CoreWSFile } from '@services/ws'; +import { CorePlatform } from '@services/platform'; /** * Component to handle a remote file. Shows the file name, icon (depending on mimetype) and a button @@ -87,7 +87,7 @@ export class CoreFileComponent implements OnInit, OnDestroy { this.fileSize = this.file.filesize; this.fileName = this.file.filename || ''; - this.isIOS = CoreApp.isIOS(); + this.isIOS = CorePlatform.isIOS(); this.defaultIsOpenWithPicker = CoreFileHelper.defaultIsOpenWithPicker(); this.openButtonIcon = this.defaultIsOpenWithPicker ? 'fas-file' : 'fas-share-square'; this.openButtonLabel = this.defaultIsOpenWithPicker ? 'core.openfile' : 'core.openwith'; diff --git a/src/core/components/loading/loading.ts b/src/core/components/loading/loading.ts index 6db45e3c8..df08396dc 100644 --- a/src/core/components/loading/loading.ts +++ b/src/core/components/loading/loading.ts @@ -18,10 +18,10 @@ import { CoreEventLoadingChangedData, CoreEvents } from '@singletons/events'; import { CoreUtils } from '@services/utils/utils'; import { CoreAnimations } from '@components/animations'; import { Translate } from '@singletons'; -import { CoreComponentsRegistry } from '@singletons/components-registry'; +import { CoreDirectivesRegistry } from '@singletons/directives-registry'; import { CorePromisedValue } from '@classes/promised-value'; -import { AsyncComponent } from '@classes/async-component'; -import { CoreApp } from '@services/app'; +import { AsyncDirective } from '@classes/async-directive'; +import { CorePlatform } from '@services/platform'; /** * Component to show a loading spinner and message while data is being loaded. @@ -49,7 +49,7 @@ import { CoreApp } from '@services/app'; styleUrls: ['loading.scss'], animations: [CoreAnimations.SHOW_HIDE], }) -export class CoreLoadingComponent implements OnInit, OnChanges, AfterViewInit, AsyncComponent, OnDestroy { +export class CoreLoadingComponent implements OnInit, OnChanges, AfterViewInit, AsyncDirective, OnDestroy { @Input() hideUntil: unknown = false; // Determine when should the contents be shown. @Input() message?: string; // Message to show while loading. @@ -65,7 +65,7 @@ export class CoreLoadingComponent implements OnInit, OnChanges, AfterViewInit, A constructor(element: ElementRef) { this.element = element.nativeElement; - CoreComponentsRegistry.register(this.element, this); + CoreDirectivesRegistry.register(this.element, this); // Calculate the unique ID. this.uniqueId = 'core-loading-content-' + CoreUtils.getUniqueId('CoreLoadingComponent'); @@ -146,7 +146,7 @@ export class CoreLoadingComponent implements OnInit, OnChanges, AfterViewInit, A if (loaded) { this.onReadyPromise.resolve(); this.restoreScrollPosition(); - if (CoreApp.isIOS()) { + if (CorePlatform.isIOS()) { this.mutationObserver.observe(this.element, { childList: true }); } } else { diff --git a/src/core/components/local-file/local-file.ts b/src/core/components/local-file/local-file.ts index 8301fbad3..62de411c7 100644 --- a/src/core/components/local-file/local-file.ts +++ b/src/core/components/local-file/local-file.ts @@ -25,8 +25,8 @@ import { CoreTextUtils } from '@services/utils/text'; import { CoreTimeUtils } from '@services/utils/time'; import { CoreUtils, CoreUtilsOpenFileOptions, OpenFileAction } from '@services/utils/utils'; import { CoreForms } from '@singletons/form'; -import { CoreApp } from '@services/app'; import { CorePath } from '@singletons/path'; +import { CorePlatform } from '@services/platform'; /** * Component to handle a local file. Only files inside the app folder can be managed. @@ -83,7 +83,7 @@ export class CoreLocalFileComponent implements OnInit { this.timemodified = CoreTimeUtils.userDate(metadata.modificationTime.getTime(), 'core.strftimedatetimeshort'); - this.isIOS = CoreApp.isIOS(); + this.isIOS = CorePlatform.isIOS(); this.defaultIsOpenWithPicker = CoreFileHelper.defaultIsOpenWithPicker(); this.openButtonIcon = this.defaultIsOpenWithPicker ? 'fas-file' : 'fas-share-square'; this.openButtonLabel = this.defaultIsOpenWithPicker ? 'core.openfile' : 'core.openwith'; diff --git a/src/core/components/navbar-buttons/navbar-buttons.ts b/src/core/components/navbar-buttons/navbar-buttons.ts index 83e18fb47..3cceba4bb 100644 --- a/src/core/components/navbar-buttons/navbar-buttons.ts +++ b/src/core/components/navbar-buttons/navbar-buttons.ts @@ -25,7 +25,7 @@ import { import { CoreLogger } from '@singletons/logger'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreContextMenuComponent } from '../context-menu/context-menu'; -import { CoreComponentsRegistry } from '@singletons/components-registry'; +import { CoreDirectivesRegistry } from '@singletons/directives-registry'; import { CoreDom } from '@singletons/dom'; const BUTTON_HIDDEN_CLASS = 'core-navbar-button-hidden'; @@ -82,7 +82,7 @@ export class CoreNavBarButtonsComponent implements OnInit, OnDestroy { this.element = element.nativeElement; this.logger = CoreLogger.getInstance('CoreNavBarButtonsComponent'); - CoreComponentsRegistry.register(this.element, this); + CoreDirectivesRegistry.register(this.element, this); } /** @@ -156,11 +156,11 @@ export class CoreNavBarButtonsComponent implements OnInit, OnDestroy { } const mainContextMenu = buttonsContainer.querySelector('core-context-menu'); - const secondaryContextMenuInstance = CoreComponentsRegistry.resolve(secondaryContextMenu, CoreContextMenuComponent); + const secondaryContextMenuInstance = CoreDirectivesRegistry.resolve(secondaryContextMenu, CoreContextMenuComponent); let mainContextMenuInstance: CoreContextMenuComponent | null; if (mainContextMenu) { // Both containers have a context menu. Merge them to prevent having 2 menus at the same time. - mainContextMenuInstance = CoreComponentsRegistry.resolve(mainContextMenu, CoreContextMenuComponent); + mainContextMenuInstance = CoreDirectivesRegistry.resolve(mainContextMenu, CoreContextMenuComponent); } else { // There is a context-menu in these buttons, but there is no main context menu in the header. // Create one main context menu dynamically. diff --git a/src/core/components/show-password/show-password.ts b/src/core/components/show-password/show-password.ts index 12289e0d7..3b61044bf 100644 --- a/src/core/components/show-password/show-password.ts +++ b/src/core/components/show-password/show-password.ts @@ -15,7 +15,7 @@ import { Component, OnInit, AfterViewInit, Input, ElementRef, ContentChild } from '@angular/core'; import { IonInput } from '@ionic/angular'; -import { CoreApp } from '@services/app'; +import { CorePlatform } from '@services/platform'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreUtils } from '@services/utils/utils'; @@ -121,7 +121,7 @@ export class CoreShowPasswordComponent implements OnInit, AfterViewInit { this.setData(this.input); // In Android, the keyboard is closed when the input type changes. Focus it again. - if (isFocused && CoreApp.isAndroid()) { + if (isFocused && CorePlatform.isAndroid()) { CoreDomUtils.focusElement(this.input); } } diff --git a/src/core/components/tabs-outlet/tabs-outlet.ts b/src/core/components/tabs-outlet/tabs-outlet.ts index 93d72b195..81f56cb08 100644 --- a/src/core/components/tabs-outlet/tabs-outlet.ts +++ b/src/core/components/tabs-outlet/tabs-outlet.ts @@ -31,7 +31,7 @@ import { CoreNavBarButtonsComponent } from '../navbar-buttons/navbar-buttons'; import { StackEvent } from '@ionic/angular/directives/navigation/stack-utils'; import { CoreNavigator } from '@services/navigator'; import { CoreTabBase, CoreTabsBaseComponent } from '@classes/tabs'; -import { CoreComponentsRegistry } from '@singletons/components-registry'; +import { CoreDirectivesRegistry } from '@singletons/directives-registry'; /** * This component displays some top scrollable tabs that will autohide on vertical scroll. @@ -207,7 +207,7 @@ export class CoreTabsOutletComponent extends CoreTabsBaseComponent { - const instance = CoreComponentsRegistry.resolve(element, CoreNavBarButtonsComponent); + const instance = CoreDirectivesRegistry.resolve(element, CoreNavBarButtonsComponent); if (instance) { const pagetagName = element.closest('.ion-page')?.tagName; diff --git a/src/core/components/tabs/tab.ts b/src/core/components/tabs/tab.ts index 3d9e40561..a93de6abc 100644 --- a/src/core/components/tabs/tab.ts +++ b/src/core/components/tabs/tab.ts @@ -16,7 +16,7 @@ import { Component, Input, Output, OnInit, OnDestroy, ElementRef, EventEmitter, import { CoreTabBase } from '@classes/tabs'; import { CoreUtils } from '@services/utils/utils'; -import { CoreComponentsRegistry } from '@singletons/components-registry'; +import { CoreDirectivesRegistry } from '@singletons/directives-registry'; import { CoreNavBarButtonsComponent } from '../navbar-buttons/navbar-buttons'; import { CoreTabsComponent } from './tabs'; @@ -140,7 +140,7 @@ export class CoreTabComponent implements OnInit, OnDestroy, CoreTabBase { protected showHideNavBarButtons(show: boolean): void { const elements = this.element.querySelectorAll('core-navbar-buttons'); elements.forEach((element) => { - const instance = CoreComponentsRegistry.resolve(element, CoreNavBarButtonsComponent); + const instance = CoreDirectivesRegistry.resolve(element, CoreNavBarButtonsComponent); if (instance) { instance.forceHide(!show); diff --git a/src/core/directives/collapsible-footer.ts b/src/core/directives/collapsible-footer.ts index e93fab948..de58fcedb 100644 --- a/src/core/directives/collapsible-footer.ts +++ b/src/core/directives/collapsible-footer.ts @@ -17,7 +17,7 @@ import { ScrollDetail } from '@ionic/core'; import { IonContent } from '@ionic/angular'; import { CoreUtils } from '@services/utils/utils'; import { CoreMath } from '@singletons/math'; -import { CoreComponentsRegistry } from '@singletons/components-registry'; +import { CoreDirectivesRegistry } from '@singletons/directives-registry'; import { CoreFormatTextDirective } from './format-text'; import { CoreEventObserver } from '@singletons/events'; import { CoreLoadingComponent } from '@components/loading/loading'; @@ -203,7 +203,7 @@ export class CoreCollapsibleFooterDirective implements OnInit, OnDestroy { * Wait until all children inside the element are done rendering. */ protected async waitFormatTextsRendered(): Promise { - await CoreComponentsRegistry.waitComponentsReady(this.element, 'core-format-text', CoreFormatTextDirective); + await CoreDirectivesRegistry.waitDirectivesReady(this.element, 'core-format-text', CoreFormatTextDirective); } /** @@ -249,8 +249,8 @@ export class CoreCollapsibleFooterDirective implements OnInit, OnDestroy { const scrollElement = await this.ionContent.getScrollElement(); await Promise.all([ - await CoreComponentsRegistry.waitComponentsReady(scrollElement, 'core-loading', CoreLoadingComponent), - await CoreComponentsRegistry.waitComponentsReady(this.element, 'core-loading', CoreLoadingComponent), + await CoreDirectivesRegistry.waitDirectivesReady(scrollElement, 'core-loading', CoreLoadingComponent), + await CoreDirectivesRegistry.waitDirectivesReady(this.element, 'core-loading', CoreLoadingComponent), ]); } diff --git a/src/core/directives/collapsible-header.ts b/src/core/directives/collapsible-header.ts index fbd6ff845..28d1c7f09 100644 --- a/src/core/directives/collapsible-header.ts +++ b/src/core/directives/collapsible-header.ts @@ -21,7 +21,7 @@ import { CoreTabsComponent } from '@components/tabs/tabs'; import { CoreSettingsHelper } from '@features/settings/services/settings-helper'; import { ScrollDetail } from '@ionic/core'; import { CoreUtils } from '@services/utils/utils'; -import { CoreComponentsRegistry } from '@singletons/components-registry'; +import { CoreDirectivesRegistry } from '@singletons/directives-registry'; import { CoreDom } from '@singletons/dom'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { CoreMath } from '@singletons/math'; @@ -294,7 +294,7 @@ export class CoreCollapsibleHeaderDirective implements OnInit, OnChanges, OnDest this.listenEvents(); // Initialize from tabs. - const tabs = CoreComponentsRegistry.resolve(this.page.querySelector('core-tabs-outlet'), CoreTabsOutletComponent); + const tabs = CoreDirectivesRegistry.resolve(this.page.querySelector('core-tabs-outlet'), CoreTabsOutletComponent); if (tabs) { const outlet = tabs.getOutlet(); @@ -424,14 +424,14 @@ export class CoreCollapsibleHeaderDirective implements OnInit, OnChanges, OnDest } // Wait loadings to finish. - await CoreComponentsRegistry.waitComponentsReady(this.page, 'core-loading', CoreLoadingComponent); + await CoreDirectivesRegistry.waitDirectivesReady(this.page, 'core-loading', CoreLoadingComponent); // Wait tabs to be ready. - await CoreComponentsRegistry.waitComponentsReady(this.page, 'core-tabs', CoreTabsComponent); - await CoreComponentsRegistry.waitComponentsReady(this.page, 'core-tabs-outlet', CoreTabsOutletComponent); + await CoreDirectivesRegistry.waitDirectivesReady(this.page, 'core-tabs', CoreTabsComponent); + await CoreDirectivesRegistry.waitDirectivesReady(this.page, 'core-tabs-outlet', CoreTabsOutletComponent); // Wait loadings to finish, inside tabs (if any). - await CoreComponentsRegistry.waitComponentsReady( + await CoreDirectivesRegistry.waitDirectivesReady( this.page, 'core-tab core-loading, ion-router-outlet core-loading', CoreLoadingComponent, @@ -445,7 +445,7 @@ export class CoreCollapsibleHeaderDirective implements OnInit, OnChanges, OnDest * @returns Promise resolved when texts are rendered. */ protected async waitFormatTextsRendered(element: Element): Promise { - await CoreComponentsRegistry.waitComponentsReady(element, 'core-format-text', CoreFormatTextDirective); + await CoreDirectivesRegistry.waitDirectivesReady(element, 'core-format-text', CoreFormatTextDirective); } /** diff --git a/src/core/directives/collapsible-item.ts b/src/core/directives/collapsible-item.ts index 635da33e6..a845e0a14 100644 --- a/src/core/directives/collapsible-item.ts +++ b/src/core/directives/collapsible-item.ts @@ -19,7 +19,7 @@ import { CoreSettingsHelper } from '@features/settings/services/settings-helper' import { CoreUtils } from '@services/utils/utils'; import { Translate } from '@singletons'; import { CoreColors } from '@singletons/colors'; -import { CoreComponentsRegistry } from '@singletons/components-registry'; +import { CoreDirectivesRegistry } from '@singletons/directives-registry'; import { CoreDom } from '@singletons/dom'; import { CoreEventObserver } from '@singletons/events'; import { Subscription } from 'rxjs'; @@ -128,14 +128,14 @@ export class CoreCollapsibleItemDirective implements OnInit, OnDestroy { return; } - await CoreComponentsRegistry.waitComponentsReady(this.page, 'core-loading', CoreLoadingComponent); + await CoreDirectivesRegistry.waitDirectivesReady(this.page, 'core-loading', CoreLoadingComponent); } /** * Wait until all children inside the element are done rendering. */ protected async waitFormatTextsRendered(): Promise { - await CoreComponentsRegistry.waitComponentsReady(this.element, 'core-format-text', CoreFormatTextDirective); + await CoreDirectivesRegistry.waitDirectivesReady(this.element, 'core-format-text', CoreFormatTextDirective); } /** diff --git a/src/core/directives/external-content.ts b/src/core/directives/external-content.ts index 68f0323e4..a4cd4afa7 100644 --- a/src/core/directives/external-content.ts +++ b/src/core/directives/external-content.ts @@ -23,7 +23,6 @@ import { EventEmitter, OnDestroy, } from '@angular/core'; -import { CoreApp } from '@services/app'; import { CoreFile } from '@services/file'; import { CoreFilepool, CoreFilepoolFileActions, CoreFilepoolFileEventData } from '@services/filepool'; import { CoreSites } from '@services/sites'; @@ -36,6 +35,10 @@ import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { CoreConstants } from '../constants'; import { CoreNetwork } from '@services/network'; import { Translate } from '@singletons'; +import { AsyncDirective } from '@classes/async-directive'; +import { CoreDirectivesRegistry } from '@singletons/directives-registry'; +import { CorePromisedValue } from '@classes/promised-value'; +import { CorePlatform } from '@services/platform'; /** * Directive to handle external content. @@ -50,7 +53,7 @@ import { Translate } from '@singletons'; @Directive({ selector: '[core-external-content]', }) -export class CoreExternalContentDirective implements AfterViewInit, OnChanges, OnDestroy { +export class CoreExternalContentDirective implements AfterViewInit, OnChanges, OnDestroy, AsyncDirective { @Input() siteId?: string; // Site ID to use. @Input() component?: string; // Component to link the file to. @@ -67,11 +70,14 @@ export class CoreExternalContentDirective implements AfterViewInit, OnChanges, O protected logger: CoreLogger; protected initialized = false; protected fileEventObserver?: CoreEventObserver; + protected onReadyPromise = new CorePromisedValue(); constructor(element: ElementRef) { this.element = element.nativeElement; this.logger = CoreLogger.getInstance('CoreExternalContentDirective'); + + CoreDirectivesRegistry.register(this.element, this); } /** @@ -111,7 +117,7 @@ export class CoreExternalContentDirective implements AfterViewInit, OnChanges, O newSource.setAttribute('src', url); if (type) { - if (CoreApp.isAndroid() && type == 'video/quicktime') { + if (CorePlatform.isAndroid() && type == 'video/quicktime') { // Fix for VideoJS/Chrome bug https://github.com/videojs/video.js/issues/423 . newSource.setAttribute('type', 'video/mp4'); } else { @@ -157,15 +163,21 @@ export class CoreExternalContentDirective implements AfterViewInit, OnChanges, O } else { this.invalid = true; + this.onReadyPromise.resolve(); return; } // Avoid handling data url's. if (url && url.indexOf('data:') === 0) { - this.invalid = true; + if (tagName === 'SOURCE') { + // Restoring original src. + this.addSource(url); + } + this.onLoad.emit(); this.loaded = true; + this.onReadyPromise.resolve(); return; } @@ -182,6 +194,8 @@ export class CoreExternalContentDirective implements AfterViewInit, OnChanges, O this.loaded = true; } } + } finally { + this.onReadyPromise.resolve(); } } @@ -266,13 +280,11 @@ export class CoreExternalContentDirective implements AfterViewInit, OnChanges, O return; } - let urls = inlineStyles.match(/https?:\/\/[^"') ;]*/g); - if (!urls || !urls.length) { + const urls = CoreUtils.uniqueArray(Array.from(inlineStyles.match(/https?:\/\/[^"') ;]*/g) ?? [])); + if (!urls.length) { return; } - urls = CoreUtils.uniqueArray(urls); // Remove duplicates. - const promises = urls.map(async (url) => { const finalUrl = await CoreFilepool.getSrcByUrl(siteId, url, this.component, this.componentId, 0, true, true); @@ -462,4 +474,11 @@ export class CoreExternalContentDirective implements AfterViewInit, OnChanges, O this.fileEventObserver?.off(); } + /** + * @inheritdoc + */ + async ready(): Promise { + return this.onReadyPromise; + } + } diff --git a/src/core/directives/format-text.ts b/src/core/directives/format-text.ts index 05e911330..b747497e6 100644 --- a/src/core/directives/format-text.ts +++ b/src/core/directives/format-text.ts @@ -42,10 +42,10 @@ import { CoreFilter, CoreFilterFilter, CoreFilterFormatTextOptions } from '@feat import { CoreFilterDelegate } from '@features/filter/services/filter-delegate'; import { CoreFilterHelper } from '@features/filter/services/filter-helper'; import { CoreSubscriptions } from '@singletons/subscriptions'; -import { CoreComponentsRegistry } from '@singletons/components-registry'; +import { CoreDirectivesRegistry } from '@singletons/directives-registry'; import { CoreCollapsibleItemDirective } from './collapsible-item'; import { CoreCancellablePromise } from '@classes/cancellable-promise'; -import { AsyncComponent } from '@classes/async-component'; +import { AsyncDirective } from '@classes/async-directive'; import { CorePath } from '@singletons/path'; import { CoreDom } from '@singletons/dom'; import { CoreEvents } from '@singletons/events'; @@ -67,7 +67,7 @@ import { FrameElementController } from '@classes/element-controllers/FrameElemen @Directive({ selector: 'core-format-text', }) -export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncComponent { +export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncDirective { @ViewChild(CoreCollapsibleItemDirective) collapsible?: CoreCollapsibleItemDirective; @@ -111,7 +111,7 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncCompo protected viewContainerRef: ViewContainerRef, @Optional() @Inject(CORE_REFRESH_CONTEXT) protected refreshContext?: CoreRefreshContext, ) { - CoreComponentsRegistry.register(element.nativeElement, this); + CoreDirectivesRegistry.register(element.nativeElement, this); this.element = element.nativeElement; this.element.classList.add('core-loading'); // Hide contents until they're treated. @@ -149,6 +149,7 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncCompo ngOnDestroy(): void { this.domElementPromise?.cancel(); this.domPromises.forEach((promise) => { promise.cancel();}); + this.elementControllers.forEach(controller => controller.destroy()); } /** @@ -365,6 +366,7 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncCompo // Move the children to the current element to be able to calculate the height. CoreDomUtils.moveChildren(result.div, this.element); + this.elementControllers.forEach(controller => controller.destroy()); this.elementControllers = result.elementControllers; await CoreUtils.nextTick(); diff --git a/src/core/features/block/classes/base-block-component.ts b/src/core/features/block/classes/base-block-component.ts index 368519223..c6073cac6 100644 --- a/src/core/features/block/classes/base-block-component.ts +++ b/src/core/features/block/classes/base-block-component.ts @@ -21,7 +21,7 @@ import { CoreCourseBlock } from '../../course/services/course'; import { Params } from '@angular/router'; import { ContextLevel } from '@/core/constants'; import { CoreNavigationOptions } from '@services/navigator'; -import { AsyncComponent } from '@classes/async-component'; +import { AsyncDirective } from '@classes/async-directive'; import { CorePromisedValue } from '@classes/promised-value'; /** @@ -30,7 +30,7 @@ import { CorePromisedValue } from '@classes/promised-value'; @Component({ template: '', }) -export abstract class CoreBlockBaseComponent implements OnInit, ICoreBlockComponent, AsyncComponent { +export abstract class CoreBlockBaseComponent implements OnInit, ICoreBlockComponent, AsyncDirective { @Input() title!: string; // The block title. @Input() block!: CoreCourseBlock; // The block to render. diff --git a/src/core/features/compile/services/compile.ts b/src/core/features/compile/services/compile.ts index 51fdcea13..0ba9a739c 100644 --- a/src/core/features/compile/services/compile.ts +++ b/src/core/features/compile/services/compile.ts @@ -79,6 +79,7 @@ import { Md5 } from 'ts-md5/dist/md5'; import { CoreSyncBaseProvider } from '@classes/base-sync'; import { CoreArray } from '@singletons/array'; import { CoreComponentsRegistry } from '@singletons/components-registry'; +import { CoreDirectivesRegistry } from '@singletons/directives-registry'; import { CoreDom } from '@singletons/dom'; import { CoreForms } from '@singletons/form'; import { CoreText } from '@singletons/text'; @@ -350,6 +351,7 @@ export class CoreCompileProvider { instance['CoreSyncBaseProvider'] = CoreSyncBaseProvider; instance['CoreArray'] = CoreArray; instance['CoreComponentsRegistry'] = CoreComponentsRegistry; + instance['CoreDirectivesRegistry'] = CoreDirectivesRegistry; instance['CoreNetwork'] = CoreNetwork.instance; instance['CorePlatform'] = CorePlatform.instance; instance['CoreDom'] = CoreDom; diff --git a/src/core/features/courses/pages/my/my.ts b/src/core/features/courses/pages/my/my.ts index ef5ae03a4..3a8e8c2b8 100644 --- a/src/core/features/courses/pages/my/my.ts +++ b/src/core/features/courses/pages/my/my.ts @@ -14,7 +14,7 @@ import { AddonBlockMyOverviewComponent } from '@addons/block/myoverview/components/myoverview/myoverview'; import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; -import { AsyncComponent } from '@classes/async-component'; +import { AsyncDirective } from '@classes/async-directive'; import { PageLoadsManager } from '@classes/page-loads-manager'; import { CorePromisedValue } from '@classes/promised-value'; import { CoreBlockComponent } from '@features/block/components/block/block'; @@ -42,7 +42,7 @@ import { CoreCourses } from '../../services/courses'; useClass: PageLoadsManager, }], }) -export class CoreCoursesMyCoursesPage implements OnInit, OnDestroy, AsyncComponent { +export class CoreCoursesMyCoursesPage implements OnInit, OnDestroy, AsyncDirective { @ViewChild(CoreBlockComponent) block!: CoreBlockComponent; diff --git a/src/core/features/editor/components/rich-text-editor/rich-text-editor.ts b/src/core/features/editor/components/rich-text-editor/rich-text-editor.ts index 6b0e660c3..5b32415bb 100644 --- a/src/core/features/editor/components/rich-text-editor/rich-text-editor.ts +++ b/src/core/features/editor/components/rich-text-editor/rich-text-editor.ts @@ -36,7 +36,7 @@ import { CoreUtils } from '@services/utils/utils'; import { Translate } from '@singletons'; import { CoreEventFormActionData, CoreEventObserver, CoreEvents } from '@singletons/events'; import { CoreEditorOffline } from '../../services/editor-offline'; -import { CoreComponentsRegistry } from '@singletons/components-registry'; +import { CoreDirectivesRegistry } from '@singletons/directives-registry'; import { CoreLoadingComponent } from '@components/loading/loading'; import { CoreScreen } from '@services/screen'; import { CoreCancellablePromise } from '@classes/cancellable-promise'; @@ -304,7 +304,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, return; } - await CoreComponentsRegistry.waitComponentsReady(page, 'core-loading', CoreLoadingComponent); + await CoreDirectivesRegistry.waitDirectivesReady(page, 'core-loading', CoreLoadingComponent); } /** diff --git a/src/core/features/fileuploader/services/fileuploader-helper.ts b/src/core/features/fileuploader/services/fileuploader-helper.ts index 30fe5ebf1..47b649a28 100644 --- a/src/core/features/fileuploader/services/fileuploader-helper.ts +++ b/src/core/features/fileuploader/services/fileuploader-helper.ts @@ -19,7 +19,6 @@ import { ChooserResult } from '@ionic-native/chooser/ngx'; import { FileEntry, IFile } from '@ionic-native/file/ngx'; import { MediaFile } from '@ionic-native/media-capture/ngx'; -import { CoreApp } from '@services/app'; import { CoreNetwork } from '@services/network'; import { CoreFile, CoreFileProvider, CoreFileProgressEvent } from '@services/file'; import { CoreDomUtils } from '@services/utils/dom'; @@ -652,7 +651,7 @@ export class CoreFileUploaderHelperProvider { options.mediaType = Camera.MediaType.PICTURE; } else if (!imageSupported && videoSupported) { options.mediaType = Camera.MediaType.VIDEO; - } else if (CoreApp.isIOS()) { + } else if (CorePlatform.isIOS()) { // Only get all media in iOS because in Android using this option allows uploading any kind of file. options.mediaType = Camera.MediaType.ALLMEDIA; } diff --git a/src/core/features/fileuploader/services/fileuploader.ts b/src/core/features/fileuploader/services/fileuploader.ts index 4949c64ff..f40ee7490 100644 --- a/src/core/features/fileuploader/services/fileuploader.ts +++ b/src/core/features/fileuploader/services/fileuploader.ts @@ -18,7 +18,6 @@ import { FileEntry } from '@ionic-native/file/ngx'; import { MediaFile, CaptureError, CaptureAudioOptions, CaptureVideoOptions } from '@ionic-native/media-capture/ngx'; import { Subject } from 'rxjs'; -import { CoreApp } from '@services/app'; import { CoreFile, CoreFileProvider } from '@services/file'; import { CoreFilepool } from '@services/filepool'; import { CoreSites } from '@services/sites'; @@ -33,6 +32,7 @@ import { CoreError } from '@classes/errors/error'; import { CoreSite } from '@classes/site'; import { CoreFileEntry, CoreFileHelper } from '@services/file-helper'; import { CorePath } from '@singletons/path'; +import { CorePlatform } from '@services/platform'; /** * File upload options. @@ -236,7 +236,7 @@ export class CoreFileUploaderProvider { getCameraUploadOptions(uri: string, isFromAlbum?: boolean): CoreFileUploaderOptions { const extension = CoreMimetypeUtils.guessExtensionFromUrl(uri); const mimetype = CoreMimetypeUtils.getMimeType(extension); - const isIOS = CoreApp.isIOS(); + const isIOS = CorePlatform.isIOS(); const options: CoreFileUploaderOptions = { deleteAfterUpload: !isFromAlbum, mimeType: mimetype, @@ -259,7 +259,7 @@ export class CoreFileUploaderProvider { // If the file was picked from the album, delete it only if it was copied to the app's folder. options.deleteAfterUpload = CoreFile.isFileInAppFolder(uri); - if (CoreApp.isAndroid()) { + if (CorePlatform.isAndroid()) { // Picking an image from album in Android adds a timestamp at the end of the file. Delete it. options.fileName = options.fileName.replace(/(\.[^.]*)\?[^.]*$/, '$1'); } diff --git a/src/core/features/fileuploader/services/handlers/audio.ts b/src/core/features/fileuploader/services/handlers/audio.ts index 99c4030ed..cdce9f086 100644 --- a/src/core/features/fileuploader/services/handlers/audio.ts +++ b/src/core/features/fileuploader/services/handlers/audio.ts @@ -45,10 +45,10 @@ export class CoreFileUploaderAudioHandlerService implements CoreFileUploaderHand * @returns Supported mimetypes. */ getSupportedMimetypes(mimetypes: string[]): string[] { - if (CoreApp.isIOS()) { + if (CorePlatform.isIOS()) { // In iOS it's recorded as WAV. return CoreUtils.filterByRegexp(mimetypes, /^audio\/wav$/); - } else if (CoreApp.isAndroid()) { + } else if (CorePlatform.isAndroid()) { // In Android we don't know the format the audio will be recorded, so accept any audio mimetype. return CoreUtils.filterByRegexp(mimetypes, /^audio\//); } else { diff --git a/src/core/features/fileuploader/services/handlers/file.ts b/src/core/features/fileuploader/services/handlers/file.ts index 54407e387..0ed301042 100644 --- a/src/core/features/fileuploader/services/handlers/file.ts +++ b/src/core/features/fileuploader/services/handlers/file.ts @@ -14,7 +14,6 @@ import { Injectable } from '@angular/core'; -import { CoreApp } from '@services/app'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreFileUploaderHandler, CoreFileUploaderHandlerData, CoreFileUploaderHandlerResult } from '../fileuploader-delegate'; import { CoreFileUploaderHelper } from '../fileuploader-helper'; @@ -94,7 +93,7 @@ export class CoreFileUploaderFileHandlerService implements CoreFileUploaderHandl const input = document.createElement('input'); input.setAttribute('type', 'file'); input.classList.add('core-fileuploader-file-handler-input'); - if (mimetypes && mimetypes.length && (!CoreApp.isAndroid() || mimetypes.length == 1)) { + if (mimetypes && mimetypes.length && (!CorePlatform.isAndroid() || mimetypes.length == 1)) { // Don't use accept attribute in Android with several mimetypes, it's not supported. input.setAttribute('accept', mimetypes.join(', ')); } @@ -134,7 +133,7 @@ export class CoreFileUploaderFileHandlerService implements CoreFileUploaderHandl } }); - if (CoreApp.isIOS()) { + if (CorePlatform.isIOS()) { // In iOS, the click on the input stopped working for some reason. We need to put it 1 level higher. element.parentElement?.appendChild(input); diff --git a/src/core/features/fileuploader/services/handlers/video.ts b/src/core/features/fileuploader/services/handlers/video.ts index b22f0750f..0472f2248 100644 --- a/src/core/features/fileuploader/services/handlers/video.ts +++ b/src/core/features/fileuploader/services/handlers/video.ts @@ -45,10 +45,10 @@ export class CoreFileUploaderVideoHandlerService implements CoreFileUploaderHand * @returns Supported mimetypes. */ getSupportedMimetypes(mimetypes: string[]): string[] { - if (CoreApp.isIOS()) { + if (CorePlatform.isIOS()) { // In iOS it's recorded as MOV. return CoreUtils.filterByRegexp(mimetypes, /^video\/quicktime$/); - } else if (CoreApp.isAndroid()) { + } else if (CorePlatform.isAndroid()) { // In Android we don't know the format the video will be recorded, so accept any video mimetype. return CoreUtils.filterByRegexp(mimetypes, /^video\//); } else { diff --git a/src/core/features/login/pages/credentials/credentials.ts b/src/core/features/login/pages/credentials/credentials.ts index c5c69bfc3..5841c1e7e 100644 --- a/src/core/features/login/pages/credentials/credentials.ts +++ b/src/core/features/login/pages/credentials/credentials.ts @@ -32,6 +32,7 @@ import { CoreUserSupport } from '@features/user/services/support'; import { CoreUserSupportConfig } from '@features/user/classes/support/support-config'; import { CoreUserGuestSupportConfig } from '@features/user/classes/support/guest-support-config'; import { SafeHtml } from '@angular/platform-browser'; +import { CorePlatform } from '@services/platform'; /** * Page to enter the user credentials. @@ -108,7 +109,7 @@ export class CoreLoginCredentialsPage implements OnInit, OnDestroy { this.pageLoaded = true; } - if (CoreApp.isIOS()) { + if (CorePlatform.isIOS()) { // Make iOS auto-fill work. The field that isn't focused doesn't get updated, do it manually. // Debounce it to prevent triggering this function too often when the user is typing. this.valueChangeSubscription = this.credForm.valueChanges.pipe(debounceTime(1000)).subscribe((changes) => { diff --git a/src/core/features/login/pages/site/site.ts b/src/core/features/login/pages/site/site.ts index 7673fa6fc..4fb73cadf 100644 --- a/src/core/features/login/pages/site/site.ts +++ b/src/core/features/login/pages/site/site.ts @@ -45,6 +45,7 @@ import { CoreErrorInfoComponent } from '@components/error-info/error-info'; import { CoreUserSupportConfig } from '@features/user/classes/support/support-config'; import { CoreUserGuestSupportConfig } from '@features/user/classes/support/guest-support-config'; import { CoreLoginError } from '@classes/errors/loginerror'; +import { CorePlatform } from '@services/platform'; /** * Site (url) chooser when adding a new site. @@ -93,7 +94,7 @@ export class CoreLoginSitePage implements OnInit { // Load fixed sites if they're set. if (CoreLoginHelper.hasSeveralFixedSites()) { url = this.initSiteSelector(); - } else if (CoreConstants.CONFIG.enableonboarding && !CoreApp.isIOS()) { + } else if (CoreConstants.CONFIG.enableonboarding && !CorePlatform.isIOS()) { this.initOnboarding(); } diff --git a/src/core/features/mainmenu/pages/menu/menu.ts b/src/core/features/mainmenu/pages/menu/menu.ts index 39211bd1b..f75066f04 100644 --- a/src/core/features/mainmenu/pages/menu/menu.ts +++ b/src/core/features/mainmenu/pages/menu/menu.ts @@ -17,7 +17,6 @@ import { IonTabs } from '@ionic/angular'; import { BackButtonEvent } from '@ionic/core'; import { Subscription } from 'rxjs'; -import { CoreApp } from '@services/app'; import { CoreEvents, CoreEventObserver } from '@singletons/events'; import { CoreMainMenu, CoreMainMenuProvider } from '../../services/mainmenu'; import { CoreMainMenuDelegate, CoreMainMenuHandlerToDisplay } from '../../services/mainmenu-delegate'; @@ -31,6 +30,7 @@ import { trigger, state, style, transition, animate } from '@angular/animations' import { CoreSites } from '@services/sites'; import { CoreDom } from '@singletons/dom'; import { CoreLogger } from '@singletons/logger'; +import { CorePlatform } from '@services/platform'; const ANIMATION_DURATION = 500; @@ -135,7 +135,7 @@ export class CoreMainMenuPage implements OnInit, OnDestroy { }); document.addEventListener('ionBackButton', this.backButtonFunction); - if (CoreApp.isIOS()) { + if (CorePlatform.isIOS()) { // In iOS, the resize event is triggered before the keyboard is opened/closed and not triggered again once done. // Init handlers again once keyboard is closed since the resize event doesn't have the updated height. this.keyboardObserver = CoreEvents.on(CoreEvents.KEYBOARD_CHANGE, (kbHeight: number) => { diff --git a/src/core/features/mainmenu/services/mainmenu.ts b/src/core/features/mainmenu/services/mainmenu.ts index 39cd39869..583d6b0da 100644 --- a/src/core/features/mainmenu/services/mainmenu.ts +++ b/src/core/features/mainmenu/services/mainmenu.ts @@ -14,7 +14,6 @@ import { Injectable } from '@angular/core'; -import { CoreApp } from '@services/app'; import { CoreLang, CoreLangLanguage } from '@services/lang'; import { CoreSites } from '@services/sites'; import { CoreConstants } from '@/core/constants'; @@ -23,6 +22,7 @@ import { Device, makeSingleton } from '@singletons'; import { CoreArray } from '@singletons/array'; import { CoreTextUtils } from '@services/utils/text'; import { CoreScreen } from '@services/screen'; +import { CorePlatform } from '@services/platform'; declare module '@singletons/events' { @@ -196,9 +196,9 @@ export class CoreMainMenuProvider { osversion: Device.version, }; - if (CoreApp.isAndroid()) { + if (CorePlatform.isAndroid()) { replacements.devicetype = 'Android'; - } else if (CoreApp.isIOS()) { + } else if (CorePlatform.isIOS()) { replacements.devicetype = 'iPhone or iPad'; } else { replacements.devicetype = 'Other'; diff --git a/src/core/features/pushnotifications/services/pushnotifications.ts b/src/core/features/pushnotifications/services/pushnotifications.ts index 583f81451..417ee3849 100644 --- a/src/core/features/pushnotifications/services/pushnotifications.ts +++ b/src/core/features/pushnotifications/services/pushnotifications.ts @@ -228,7 +228,7 @@ export class CorePushNotificationsProvider { * @returns Promise resolved when done. */ protected async createDefaultChannel(): Promise { - if (!CoreApp.isAndroid()) { + if (!CorePlatform.isAndroid()) { return; } @@ -481,7 +481,7 @@ export class CorePushNotificationsProvider { text: notification.message, channel: 'PushPluginChannel', }; - const isAndroid = CoreApp.isAndroid(); + const isAndroid = CorePlatform.isAndroid(); const extraFeatures = CoreUtils.isTrueOrOne(data.extrafeatures); if (extraFeatures && isAndroid && CoreUtils.isFalseOrZero(data.notif)) { diff --git a/src/core/features/question/components/question/question.ts b/src/core/features/question/components/question/question.ts index af9c2d08e..9d6d349ef 100644 --- a/src/core/features/question/components/question/question.ts +++ b/src/core/features/question/components/question/question.ts @@ -13,7 +13,7 @@ // limitations under the License. import { Component, Input, Output, OnInit, EventEmitter, ChangeDetectorRef, Type, ElementRef } from '@angular/core'; -import { AsyncComponent } from '@classes/async-component'; +import { AsyncDirective } from '@classes/async-directive'; import { CorePromisedValue } from '@classes/promised-value'; import { CoreQuestionBehaviourDelegate } from '@features/question/services/behaviour-delegate'; import { CoreQuestionDelegate } from '@features/question/services/question-delegate'; @@ -22,7 +22,7 @@ import { CoreQuestionBehaviourButton, CoreQuestionHelper, CoreQuestionQuestion } import { CoreDomUtils } from '@services/utils/dom'; import { CoreUtils } from '@services/utils/utils'; import { Translate } from '@singletons'; -import { CoreComponentsRegistry } from '@singletons/components-registry'; +import { CoreDirectivesRegistry } from '@singletons/directives-registry'; import { CoreLogger } from '@singletons/logger'; /** @@ -33,7 +33,7 @@ import { CoreLogger } from '@singletons/logger'; templateUrl: 'core-question.html', styleUrls: ['../../question.scss'], }) -export class CoreQuestionComponent implements OnInit, AsyncComponent { +export class CoreQuestionComponent implements OnInit, AsyncDirective { @Input() question?: CoreQuestionQuestion; // The question to render. @Input() component?: string; // The component the question belongs to. @@ -66,7 +66,7 @@ export class CoreQuestionComponent implements OnInit, AsyncComponent { constructor(protected changeDetector: ChangeDetectorRef, private element: ElementRef) { this.logger = CoreLogger.getInstance('CoreQuestionComponent'); this.promisedReady = new CorePromisedValue(); - CoreComponentsRegistry.register(this.element.nativeElement, this); + CoreDirectivesRegistry.register(this.element.nativeElement, this); } async ready(): Promise { diff --git a/src/core/features/settings/classes/settings-sections-source.ts b/src/core/features/settings/classes/settings-sections-source.ts index d3bcd02c9..caf755488 100644 --- a/src/core/features/settings/classes/settings-sections-source.ts +++ b/src/core/features/settings/classes/settings-sections-source.ts @@ -16,7 +16,7 @@ import { CoreConstants } from '@/core/constants'; import { Params } from '@angular/router'; import { CoreRoutedItemsManagerSource } from '@classes/items-management/routed-items-manager-source'; import { SHAREDFILES_PAGE_NAME } from '@features/sharedfiles/sharedfiles.module'; -import { CoreApp } from '@services/app'; +import { CorePlatform } from '@services/platform'; /** * Provides a collection of setting sections. @@ -45,7 +45,7 @@ export class CoreSettingsSectionsSource extends CoreRoutedItemsManagerSource { - return CoreApp.isIOS(); + return CorePlatform.isIOS(); } /** diff --git a/src/core/features/sharedfiles/services/handlers/upload.ts b/src/core/features/sharedfiles/services/handlers/upload.ts index a32da22fd..c0ff37195 100644 --- a/src/core/features/sharedfiles/services/handlers/upload.ts +++ b/src/core/features/sharedfiles/services/handlers/upload.ts @@ -19,7 +19,7 @@ import { CoreFileUploaderHandlerData, CoreFileUploaderHandlerResult, } from '@features/fileuploader/services/fileuploader-delegate'; -import { CoreApp } from '@services/app'; +import { CorePlatform } from '@services/platform'; import { makeSingleton } from '@singletons'; import { CoreSharedFilesHelper } from '../sharedfiles-helper'; /** @@ -37,7 +37,7 @@ export class CoreSharedFilesUploadHandlerService implements CoreFileUploaderHand * @returns True or promise resolved with true if enabled. */ async isEnabled(): Promise { - return CoreApp.isIOS(); + return CorePlatform.isIOS(); } /** diff --git a/src/core/features/sharedfiles/services/sharedfiles-helper.ts b/src/core/features/sharedfiles/services/sharedfiles-helper.ts index c3ab8d4c3..4fa26d913 100644 --- a/src/core/features/sharedfiles/services/sharedfiles-helper.ts +++ b/src/core/features/sharedfiles/services/sharedfiles-helper.ts @@ -18,7 +18,6 @@ import { FileEntry } from '@ionic-native/file/ngx'; import { CoreCanceledError } from '@classes/errors/cancelederror'; import { CoreFileUploader } from '@features/fileuploader/services/fileuploader'; import { CoreFileUploaderHandlerResult } from '@features/fileuploader/services/fileuploader-delegate'; -import { CoreApp } from '@services/app'; import { CoreFile } from '@services/file'; import { CoreNavigator } from '@services/navigator'; import { CoreSites } from '@services/sites'; @@ -49,7 +48,7 @@ export class CoreSharedFilesHelperProvider { * Initialize. */ initialize(): void { - if (!CoreApp.isIOS()) { + if (!CorePlatform.isIOS()) { return; } diff --git a/src/core/features/siteplugins/services/siteplugins.ts b/src/core/features/siteplugins/services/siteplugins.ts index 6cc006cd4..77247e9b7 100644 --- a/src/core/features/siteplugins/services/siteplugins.ts +++ b/src/core/features/siteplugins/services/siteplugins.ts @@ -97,7 +97,7 @@ export class CoreSitePluginsProvider { }; if (args.appismobile) { - defaultArgs.appplatform = CoreApp.isIOS() ? 'ios' : 'android'; + defaultArgs.appplatform = CorePlatform.isIOS() ? 'ios' : 'android'; } return { diff --git a/src/core/features/usertours/components/user-tour/user-tour.ts b/src/core/features/usertours/components/user-tour/user-tour.ts index 5fcb9c2eb..2f278d8fc 100644 --- a/src/core/features/usertours/components/user-tour/user-tour.ts +++ b/src/core/features/usertours/components/user-tour/user-tour.ts @@ -30,7 +30,7 @@ import { CoreUserToursPopoverLayout } from '@features/usertours/classes/popover- import { CoreUserTours, CoreUserToursAlignment, CoreUserToursSide } from '@features/usertours/services/user-tours'; import { CoreDomUtils } from '@services/utils/dom'; import { AngularFrameworkDelegate } from '@singletons'; -import { CoreComponentsRegistry } from '@singletons/components-registry'; +import { CoreDirectivesRegistry } from '@singletons/directives-registry'; import { CoreDom } from '@singletons/dom'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { CoreMainMenuProvider } from '@features/mainmenu/services/mainmenu'; @@ -84,7 +84,7 @@ export class CoreUserToursUserTourComponent implements AfterViewInit, OnDestroy constructor({ nativeElement: element }: ElementRef) { this.element = element; - CoreComponentsRegistry.register(element, this); + CoreDirectivesRegistry.register(element, this); this.element.addEventListener('click', (event) => this.dismissOnBackOrBackdrop(event.target as HTMLElement)); diff --git a/src/core/features/usertours/services/user-tours.ts b/src/core/features/usertours/services/user-tours.ts index 2c3712d31..a79b60f93 100644 --- a/src/core/features/usertours/services/user-tours.ts +++ b/src/core/features/usertours/services/user-tours.ts @@ -21,7 +21,7 @@ import { CoreDatabaseCachingStrategy, CoreDatabaseTableProxy } from '@classes/da import { CoreApp } from '@services/app'; import { CoreUtils } from '@services/utils/utils'; import { AngularFrameworkDelegate, makeSingleton } from '@singletons'; -import { CoreComponentsRegistry } from '@singletons/components-registry'; +import { CoreDirectivesRegistry } from '@singletons/directives-registry'; import { CoreDom } from '@singletons/dom'; import { CoreSubscriptions } from '@singletons/subscriptions'; import { CoreUserToursUserTourComponent } from '../components/user-tour/user-tour'; @@ -120,7 +120,7 @@ export class CoreUserToursService { CoreUserToursUserTourComponent, { ...componentOptions, container }, ); - const tour = CoreComponentsRegistry.require(element, CoreUserToursUserTourComponent); + const tour = CoreDirectivesRegistry.require(element, CoreUserToursUserTourComponent); return this.startTour(tour, options.watch ?? (options as CoreUserToursFocusedOptions).focus); } diff --git a/src/core/initializers/inject-ios-scripts.ts b/src/core/initializers/inject-ios-scripts.ts index eac4cb3b6..36affed3f 100644 --- a/src/core/initializers/inject-ios-scripts.ts +++ b/src/core/initializers/inject-ios-scripts.ts @@ -12,14 +12,16 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { CoreApp } from '@services/app'; import { CorePlatform } from '@services/platform'; import { CoreIframeUtils } from '@services/utils/iframe'; +/** + * Inject some scripts for iOS iframes. + */ export default async function(): Promise { await CorePlatform.ready(); - if (!CoreApp.isIOS() || !('WKUserScript' in window)) { + if (!CorePlatform.isIOS() || !('WKUserScript' in window)) { return; } diff --git a/src/core/initializers/prepare-inapp-browser.ts b/src/core/initializers/prepare-inapp-browser.ts index 026e6115c..18be057f6 100644 --- a/src/core/initializers/prepare-inapp-browser.ts +++ b/src/core/initializers/prepare-inapp-browser.ts @@ -16,7 +16,7 @@ import { CoreSiteError } from '@classes/errors/siteerror'; import { CoreLoginHelper } from '@features/login/services/login-helper'; import { CoreUserAuthenticatedSupportConfig } from '@features/user/classes/support/authenticated-support-config'; import { CoreUserNullSupportConfig } from '@features/user/classes/support/null-support-config'; -import { CoreApp } from '@services/app'; +import { CorePlatform } from '@services/platform'; import { CoreSites } from '@services/sites'; import { CoreCustomURLSchemes } from '@services/urlschemes'; import { CoreDomUtils } from '@services/utils/dom'; @@ -27,6 +27,9 @@ import { CoreEvents } from '@singletons/events'; let lastInAppUrl: string | null = null; +/** + * + */ export default function(): void { // Check URLs loaded in any InAppBrowser. CoreEvents.on(CoreEvents.IAB_LOAD_START, async (event) => { @@ -59,7 +62,7 @@ export default function(): void { return; } - if (!CoreApp.isAndroid()) { + if (!CorePlatform.isAndroid()) { return; } diff --git a/src/core/services/app.ts b/src/core/services/app.ts index fa6315372..56263b143 100644 --- a/src/core/services/app.ts +++ b/src/core/services/app.ts @@ -18,7 +18,7 @@ import { CoreDB } from '@services/db'; import { CoreEvents } from '@singletons/events'; import { SQLiteDB, SQLiteDBTableSchema } from '@classes/sqlitedb'; -import { makeSingleton, Keyboard, StatusBar, Device } from '@singletons'; +import { makeSingleton, Keyboard, StatusBar } from '@singletons'; import { CoreLogger } from '@singletons/logger'; import { CoreColors } from '@singletons/colors'; import { DBNAME, SCHEMA_VERSIONS_TABLE_NAME, SCHEMA_VERSIONS_TABLE_SCHEMA, SchemaVersionsDBEntry } from '@services/database/app'; @@ -204,7 +204,7 @@ export class CoreAppProvider { return 'itms-apps://itunes.apple.com/app/' + storesConfig.ios; } - if (this.isAndroid() && storesConfig.android) { + if (CorePlatform.isAndroid() && storesConfig.android) { return 'market://details?id=' + storesConfig.android; } @@ -219,13 +219,10 @@ export class CoreAppProvider { * Get platform major version number. * * @returns The platform major number. + * @deprecated since 4.1.1. Use CorePlatform.getPlatformMajorVersion instead. */ getPlatformMajorVersion(): number { - if (!CorePlatform.isMobile()) { - return 0; - } - - return Number(Device.version?.split('.')[0]); + return CorePlatform.getPlatformMajorVersion(); } /** @@ -242,9 +239,10 @@ export class CoreAppProvider { * Checks if the app is running in an Android mobile or tablet device. * * @returns Whether the app is running in an Android mobile or tablet device. + * @deprecated since 4.1.1. Use CorePlatform.isAndroid instead. */ isAndroid(): boolean { - return CorePlatform.isMobile() && CorePlatform.is('android'); + return CorePlatform.isAndroid(); } /** @@ -261,9 +259,10 @@ export class CoreAppProvider { * Checks if the app is running in an iOS mobile or tablet device. * * @returns Whether the app is running in an iOS mobile or tablet device. + * @deprecated since 4.1.1. Use CorePlatform.isIOS instead. */ isIOS(): boolean { - return CorePlatform.isMobile() && !CorePlatform.is('android'); + return CorePlatform.isIOS(); } /** @@ -387,7 +386,7 @@ export class CoreAppProvider { */ openKeyboard(): void { // Open keyboard is not supported in desktop and in iOS. - if (this.isAndroid()) { + if (CorePlatform.isAndroid()) { Keyboard.show(); } } diff --git a/src/core/services/file-helper.ts b/src/core/services/file-helper.ts index 0d6404f01..93825ee38 100644 --- a/src/core/services/file-helper.ts +++ b/src/core/services/file-helper.ts @@ -15,7 +15,6 @@ import { Injectable } from '@angular/core'; import { FileEntry } from '@ionic-native/file/ngx'; -import { CoreApp } from '@services/app'; import { CoreNetwork } from '@services/network'; import { CoreFile } from '@services/file'; import { CoreFilepool } from '@services/filepool'; @@ -31,6 +30,7 @@ import { CoreNetworkError } from '@classes/errors/network-error'; import { CoreConfig } from './config'; import { CoreCanceledError } from '@classes/errors/cancelederror'; import { CoreMimetypeUtils } from '@services/utils/mimetype'; +import { CorePlatform } from './platform'; /** * Provider to provide some helper functions regarding files and packages. @@ -44,7 +44,7 @@ export class CoreFileHelperProvider { * @returns Boolean. */ defaultIsOpenWithPicker(): boolean { - return CoreApp.isIOS() && CoreConstants.CONFIG.iOSDefaultOpenFileAction === OpenFileAction.OPEN_WITH; + return CorePlatform.isIOS() && CoreConstants.CONFIG.iOSDefaultOpenFileAction === OpenFileAction.OPEN_WITH; } /** diff --git a/src/core/services/file.ts b/src/core/services/file.ts index f03ac2e65..cc45ae0d2 100644 --- a/src/core/services/file.ts +++ b/src/core/services/file.ts @@ -16,7 +16,6 @@ import { Injectable } from '@angular/core'; import { FileEntry, DirectoryEntry, Entry, Metadata, IFile } from '@ionic-native/file/ngx'; -import { CoreApp } from '@services/app'; import { CoreMimetypeUtils } from '@services/utils/mimetype'; import { CoreTextUtils } from '@services/utils/text'; import { CoreUtils } from '@services/utils/utils'; @@ -140,9 +139,9 @@ export class CoreFileProvider { await CorePlatform.ready(); - if (CoreApp.isAndroid()) { + if (CorePlatform.isAndroid()) { this.basePath = File.externalApplicationStorageDirectory || this.basePath; - } else if (CoreApp.isIOS()) { + } else if (CorePlatform.isIOS()) { this.basePath = File.documentsDirectory || this.basePath; } else if (!this.isAvailable() || this.basePath === '') { this.logger.error('Error getting device OS.'); @@ -441,7 +440,7 @@ export class CoreFileProvider { */ calculateFreeSpace(): Promise { return File.getFreeDiskSpace().then((size) => { - if (CoreApp.isIOS()) { + if (CorePlatform.isIOS()) { // In iOS the size is in bytes. return Number(size); } @@ -717,7 +716,7 @@ export class CoreFileProvider { async getBasePathToDownload(): Promise { await this.init(); - if (CoreApp.isIOS()) { + if (CorePlatform.isIOS()) { // In iOS we want the internal URL (cdvfile://localhost/persistent/...). const dirEntry = await File.resolveDirectoryUrl(this.basePath); @@ -1263,7 +1262,7 @@ export class CoreFileProvider { return src; } - if (CoreApp.isIOS()) { + if (CorePlatform.isIOS()) { return src.replace(CoreConstants.CONFIG.ioswebviewscheme + '://localhost/_app_file_', 'file://'); } diff --git a/src/core/services/geolocation.ts b/src/core/services/geolocation.ts index bea6a4df5..e736cdba8 100644 --- a/src/core/services/geolocation.ts +++ b/src/core/services/geolocation.ts @@ -74,7 +74,7 @@ export class CoreGeolocationProvider { return; } - if (!CoreApp.isIOS()) { + if (!CorePlatform.isIOS()) { Diagnostic.switchToLocationSettings(); await CoreApp.waitForResume(30000); @@ -142,7 +142,7 @@ export class CoreGeolocationProvider { * Request and return the location authorization status for the application. */ protected async requestLocationAuthorization(): Promise { - if (!CoreApp.isIOS()) { + if (!CorePlatform.isIOS()) { await Diagnostic.requestLocationAuthorization(); return; diff --git a/src/core/services/local-notifications.ts b/src/core/services/local-notifications.ts index 7b9bc100a..2b3e9d820 100644 --- a/src/core/services/local-notifications.ts +++ b/src/core/services/local-notifications.ts @@ -186,7 +186,7 @@ export class CoreLocalNotificationsProvider { */ canDisableSound(): boolean { // Only allow disabling sound in Android 7 or lower. In iOS and Android 8+ it can easily be done with system settings. - return this.isAvailable() && CoreApp.isAndroid() && CoreApp.getPlatformMajorVersion() < 8; + return CorePlatform.isAndroid() && CorePlatform.getPlatformMajorVersion() < 8; } /** @@ -195,7 +195,7 @@ export class CoreLocalNotificationsProvider { * @returns Promise resolved when done. */ protected async createDefaultChannel(): Promise { - if (!CoreApp.isAndroid()) { + if (!CorePlatform.isAndroid()) { return; } @@ -583,7 +583,7 @@ export class CoreLocalNotificationsProvider { notification.data.component = component; notification.data.siteId = siteId; - if (CoreApp.isAndroid()) { + if (CorePlatform.isAndroid()) { notification.icon = notification.icon || 'res://icon'; notification.smallIcon = notification.smallIcon || 'res://smallicon'; notification.color = notification.color || CoreConstants.CONFIG.notificoncolor; diff --git a/src/core/services/platform.ts b/src/core/services/platform.ts index bb157d9f6..c00787d1b 100644 --- a/src/core/services/platform.ts +++ b/src/core/services/platform.ts @@ -14,7 +14,7 @@ import { Injectable } from '@angular/core'; import { Platform } from '@ionic/angular'; -import { makeSingleton } from '@singletons'; +import { Device, makeSingleton } from '@singletons'; /** * Extend Ionic's Platform service. @@ -22,6 +22,55 @@ import { makeSingleton } from '@singletons'; @Injectable({ providedIn: 'root' }) export class CorePlatformService extends Platform { + /** + * Get platform major version number. + * + * @returns The platform major number. + */ + getPlatformMajorVersion(): number { + if (!this.isMobile()) { + return 0; + } + + return Number(Device.version?.split('.')[0]); + } + + /** + * Checks if the app is running in an Android mobile or tablet device. + * + * @returns Whether the app is running in an Android mobile or tablet device. + */ + isAndroid(): boolean { + return this.isMobile() && this.is('android'); + } + + /** + * Checks if the app is running in an iOS mobile or tablet device. + * + * @returns Whether the app is running in an iOS mobile or tablet device. + */ + isIOS(): boolean { + return this.isMobile() && !this.is('android'); + } + + /** + * Checks if the app is running in an iPad device. + * + * @returns Whether the app is running in an iPad device. + */ + isIPad(): boolean { + return this.isIOS() && this.is('ipad'); + } + + /** + * Checks if the app is running in an iPhone device. + * + * @returns Whether the app is running in an iPhone device. + */ + isIPhone(): boolean { + return this.isIOS() && this.is('iphone'); + } + /** * Checks if the app is running in a mobile or tablet device (Cordova). * diff --git a/src/core/services/tests/utils/text.test.ts b/src/core/services/tests/utils/text.test.ts index 6d11a1050..67db67650 100644 --- a/src/core/services/tests/utils/text.test.ts +++ b/src/core/services/tests/utils/text.test.ts @@ -12,11 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { CoreApp } from '@services/app'; import { CoreTextUtilsProvider } from '@services/utils/text'; import { DomSanitizer } from '@singletons'; import { mockSingleton } from '@/testing/utils'; +import { CorePlatform } from '@services/platform'; describe('CoreTextUtilsProvider', () => { @@ -24,7 +24,7 @@ describe('CoreTextUtilsProvider', () => { let textUtils: CoreTextUtilsProvider; beforeEach(() => { - mockSingleton(CoreApp, [], { isAndroid: () => config.platform === 'android' }); + mockSingleton(CorePlatform, [], { isAndroid: () => config.platform === 'android' }); mockSingleton(DomSanitizer, [], { bypassSecurityTrustUrl: url => url }); textUtils = new CoreTextUtilsProvider(); @@ -57,7 +57,7 @@ describe('CoreTextUtilsProvider', () => { expect(url).toEqual('geo:0,0?q=Moodle%20Spain%20HQ'); expect(DomSanitizer.bypassSecurityTrustUrl).toHaveBeenCalled(); - expect(CoreApp.isAndroid).toHaveBeenCalled(); + expect(CorePlatform.isAndroid).toHaveBeenCalled(); }); it('builds address URL for non-Android platforms', () => { @@ -73,7 +73,7 @@ describe('CoreTextUtilsProvider', () => { expect(url).toEqual('http://maps.google.com?q=Moodle%20Spain%20HQ'); expect(DomSanitizer.bypassSecurityTrustUrl).toHaveBeenCalled(); - expect(CoreApp.isAndroid).toHaveBeenCalled(); + expect(CorePlatform.isAndroid).toHaveBeenCalled(); }); it('doesn\'t build address if it\'s already a URL', () => { diff --git a/src/core/services/utils/dom.ts b/src/core/services/utils/dom.ts index bca8e0701..f91e453e4 100644 --- a/src/core/services/utils/dom.ts +++ b/src/core/services/utils/dom.ts @@ -51,12 +51,13 @@ import { CoreSites } from '@services/sites'; import { NavigationStart } from '@angular/router'; import { filter } from 'rxjs/operators'; import { Subscription } from 'rxjs'; -import { CoreComponentsRegistry } from '@singletons/components-registry'; +import { CoreDirectivesRegistry } from '@singletons/directives-registry'; import { CoreDom } from '@singletons/dom'; import { CoreNetwork } from '@services/network'; import { CoreSiteError } from '@classes/errors/siteerror'; import { CoreUserSupport } from '@features/user/services/support'; import { CoreErrorInfoComponent } from '@components/error-info/error-info'; +import { CorePlatform } from '@services/platform'; /* * "Utils" service with helper functions for UI, DOM elements and HTML code. @@ -133,7 +134,7 @@ export class CoreDomUtilsProvider { const getAvailableBytes = async (): Promise => { const availableBytes = await CoreFile.calculateFreeSpace(); - if (CoreApp.isAndroid()) { + if (CorePlatform.isAndroid()) { return availableBytes; } else { // Space calculation is not accurate on iOS, but it gets more accurate when space is lower. @@ -152,7 +153,7 @@ export class CoreDomUtilsProvider { } else { const availableSize = CoreTextUtils.bytesToSize(availableBytes, 2); - if (CoreApp.isAndroid() && size.size > availableBytes - CoreConstants.MINIMUM_FREE_SPACE) { + if (CorePlatform.isAndroid() && size.size > availableBytes - CoreConstants.MINIMUM_FREE_SPACE) { throw new CoreError( Translate.instant( 'core.course.insufficientavailablespace', @@ -338,7 +339,7 @@ export class CoreDomUtilsProvider { if (focusElement === document.activeElement) { await CoreUtils.nextTick(); - if (CoreApp.isAndroid() && this.supportsInputKeyboard(focusElement)) { + if (CorePlatform.isAndroid() && this.supportsInputKeyboard(focusElement)) { // On some Android versions the keyboard doesn't open automatically. CoreApp.openKeyboard(); } @@ -665,10 +666,10 @@ export class CoreDomUtilsProvider { * * @param element The root element of the component/directive. * @returns The instance, undefined if not found. - * @deprecated since 4.0.0. Use CoreComponentsRegistry instead. + * @deprecated since 4.0.0. Use CoreDirectivesRegistry instead. */ getInstanceByElement(element: Element): T | undefined { - return CoreComponentsRegistry.resolve(element) ?? undefined; + return CoreDirectivesRegistry.resolve(element) ?? undefined; } /** @@ -1529,7 +1530,7 @@ export class CoreDomUtilsProvider { buttons: buttons, }); - const isDevice = CoreApp.isAndroid() || CoreApp.isIOS(); + const isDevice = CorePlatform.isAndroid() || CorePlatform.isIOS(); if (!isDevice) { // Treat all anchors so they don't override the app. const alertMessageEl: HTMLElement | null = alert.querySelector('.alert-message'); @@ -1718,10 +1719,10 @@ export class CoreDomUtilsProvider { * * @param element The root element of the component/directive. * @param instance The instance to store. - * @deprecated since 4.0.0. Use CoreComponentsRegistry instead. + * @deprecated since 4.0.0. Use CoreDirectivesRegistry instead. */ storeInstanceByElement(element: Element, instance: unknown): void { - CoreComponentsRegistry.register(element, instance); + CoreDirectivesRegistry.register(element, instance); } /** @@ -1989,7 +1990,7 @@ export class CoreDomUtilsProvider { * @returns Promise resolved when done. */ async waitForResizeDone(windowWidth?: number, windowHeight?: number, retries = 0): Promise { - if (!CoreApp.isIOS()) { + if (!CorePlatform.isIOS()) { return; // Only wait in iOS. } diff --git a/src/core/services/utils/iframe.ts b/src/core/services/utils/iframe.ts index fc3a05e25..1d79a73f0 100644 --- a/src/core/services/utils/iframe.ts +++ b/src/core/services/utils/iframe.ts @@ -16,7 +16,6 @@ import { Injectable } from '@angular/core'; import { WKUserScriptWindow } from 'cordova-plugin-wkuserscript'; import { WKWebViewCookiesWindow } from 'cordova-plugin-wkwebview-cookies'; -import { CoreApp } from '@services/app'; import { CoreNetwork } from '@services/network'; import { CoreFile } from '@services/file'; import { CoreFileHelper } from '@services/file-helper'; @@ -32,6 +31,7 @@ import { CoreWindow } from '@singletons/window'; import { CoreContentLinksHelper } from '@features/contentlinks/services/contentlinks-helper'; import { CorePath } from '@singletons/path'; import { CorePromisedValue } from '@classes/promised-value'; +import { CorePlatform } from '@services/platform'; /** * Possible types of frame elements. @@ -531,7 +531,7 @@ export class CoreIframeUtilsProvider { } catch (error) { CoreDomUtils.showErrorModal(error); } - } else if (CoreApp.isIOS() && (!link.target || link.target == '_self') && element) { + } else if (CorePlatform.isIOS() && (!link.target || link.target == '_self') && element) { // In cordova ios 4.1.0 links inside iframes stopped working. We'll manually treat them. event && event.preventDefault(); if (element.tagName.toLowerCase() == 'object') { @@ -564,7 +564,7 @@ export class CoreIframeUtilsProvider { * @returns Promise resolved when done. */ async fixIframeCookies(url: string): Promise { - if (!CoreApp.isIOS() || !url || CoreUrlUtils.isLocalFileUrl(url)) { + if (!CorePlatform.isIOS() || !url || CoreUrlUtils.isLocalFileUrl(url)) { // No need to fix cookies. return; } @@ -593,7 +593,7 @@ export class CoreIframeUtilsProvider { * @returns Boolean. */ shouldDisplayHelp(): boolean { - return CoreApp.isIOS() && CoreApp.getPlatformMajorVersion() >= 14; + return CorePlatform.isIOS() && CorePlatform.getPlatformMajorVersion() >= 14; } /** diff --git a/src/core/services/utils/mimetype.ts b/src/core/services/utils/mimetype.ts index bf9105362..a22e98750 100644 --- a/src/core/services/utils/mimetype.ts +++ b/src/core/services/utils/mimetype.ts @@ -25,6 +25,7 @@ import { CoreUtils } from '@services/utils/utils'; import extToMime from '@/assets/exttomime.json'; import mimeToExt from '@/assets/mimetoext.json'; import { CoreFileEntry, CoreFileHelper } from '@services/file-helper'; +import { CoreUrl } from '@singletons/url'; interface MimeTypeInfo { type: string; @@ -302,18 +303,13 @@ export class CoreMimetypeUtilsProvider { * @returns The lowercased extension without the dot, or undefined. */ guessExtensionFromUrl(fileUrl: string): string | undefined { - const split = fileUrl.split('.'); + const split = CoreUrl.removeUrlAnchor(fileUrl).split('.'); let extension: string | undefined; if (split.length > 1) { let candidate = split[split.length - 1].toLowerCase(); // Remove params if any. - let position = candidate.indexOf('?'); - if (position > -1) { - candidate = candidate.substring(0, position); - } - // Remove anchor if any. - position = candidate.indexOf('#'); + const position = candidate.indexOf('?'); if (position > -1) { candidate = candidate.substring(0, position); } diff --git a/src/core/services/utils/text.ts b/src/core/services/utils/text.ts index 24065d5cc..96fdce92a 100644 --- a/src/core/services/utils/text.ts +++ b/src/core/services/utils/text.ts @@ -16,7 +16,6 @@ import { Injectable } from '@angular/core'; import { SafeUrl } from '@angular/platform-browser'; import { ModalOptions } from '@ionic/core'; -import { CoreApp } from '@services/app'; import { CoreAnyError, CoreError } from '@classes/errors/error'; import { DomSanitizer, makeSingleton, Translate } from '@singletons'; import { CoreWSFile } from '@services/ws'; @@ -28,6 +27,7 @@ import { CoreText } from '@singletons/text'; import { CoreUrl } from '@singletons/url'; import { AlertButton } from '@ionic/angular'; import { CorePath } from '@singletons/path'; +import { CorePlatform } from '@services/platform'; /** * Different type of errors the app can treat. @@ -187,7 +187,7 @@ export class CoreTextUtilsProvider { return DomSanitizer.bypassSecurityTrustUrl(address); } - return DomSanitizer.bypassSecurityTrustUrl((CoreApp.isAndroid() ? 'geo:0,0?q=' : 'http://maps.google.com?q=') + + return DomSanitizer.bypassSecurityTrustUrl((CorePlatform.isAndroid() ? 'geo:0,0?q=' : 'http://maps.google.com?q=') + encodeURIComponent(address)); } diff --git a/src/core/services/utils/url.ts b/src/core/services/utils/url.ts index 71ab26903..c7c63dec4 100644 --- a/src/core/services/utils/url.ts +++ b/src/core/services/utils/url.ts @@ -22,6 +22,7 @@ import { CoreUrl } from '@singletons/url'; import { CoreSites } from '@services/sites'; import { CorePath } from '@singletons/path'; import { CorePlatform } from '@services/platform'; +import { CoreMedia } from '@singletons/media'; /* * "Utils" service with helper functions for URLs. @@ -120,7 +121,8 @@ export class CoreUrlUtilsProvider { // Also, only use it for "core" pluginfile endpoints. Some plugins can implement their own endpoint (like customcert). return !CoreConstants.CONFIG.disableTokenFile && !!accessKey && !url.match(/[&?]file=/) && ( url.indexOf(CorePath.concatenatePaths(siteUrl, 'pluginfile.php')) === 0 || - url.indexOf(CorePath.concatenatePaths(siteUrl, 'webservice/pluginfile.php')) === 0); + url.indexOf(CorePath.concatenatePaths(siteUrl, 'webservice/pluginfile.php')) === 0) && + !CoreMedia.sourceUsesJavascriptPlayer({ src: url }); } /** diff --git a/src/core/services/utils/utils.ts b/src/core/services/utils/utils.ts index a55bd56ff..555b6eb14 100644 --- a/src/core/services/utils/utils.ts +++ b/src/core/services/utils/utils.ts @@ -17,7 +17,6 @@ import { InAppBrowserObject, InAppBrowserOptions } from '@ionic-native/in-app-br import { FileEntry } from '@ionic-native/file/ngx'; import { Subscription } from 'rxjs'; -import { CoreApp } from '@services/app'; import { CoreEvents } from '@singletons/events'; import { CoreFile } from '@services/file'; import { CoreLang } from '@services/lang'; @@ -995,12 +994,12 @@ export class CoreUtilsProvider { const extension = CoreMimetypeUtils.getFileExtension(path); const mimetype = extension && CoreMimetypeUtils.getMimeType(extension); - if (mimetype == 'text/html' && CoreApp.isAndroid()) { + if (mimetype == 'text/html' && CorePlatform.isAndroid()) { // Open HTML local files in InAppBrowser, in system browser some embedded files aren't loaded. this.openInApp(path); return; - } else if (extension === 'apk' && CoreApp.isAndroid()) { + } else if (extension === 'apk' && CorePlatform.isAndroid()) { const url = await CoreUtils.ignoreErrors( CoreFilepool.getFileUrlByPath(CoreSites.getCurrentSiteId(), CoreFile.removeBasePath(path)), ); @@ -1065,7 +1064,7 @@ export class CoreUtilsProvider { options.enableViewPortScale = options.enableViewPortScale ?? 'yes'; // Enable zoom on iOS by default. options.allowInlineMediaPlayback = options.allowInlineMediaPlayback ?? 'yes'; // Allow playing inline videos in iOS. - if (!options.location && CoreApp.isIOS() && url.indexOf('file://') === 0) { + if (!options.location && CorePlatform.isIOS() && url.indexOf('file://') === 0) { // The URL uses file protocol, don't show it on iOS. // In Android we keep it because otherwise we lose the whole toolbar. options.location = 'no'; @@ -1190,7 +1189,7 @@ export class CoreUtilsProvider { * @returns Promise resolved when opened. */ async openOnlineFile(url: string): Promise { - if (CoreApp.isAndroid()) { + if (CorePlatform.isAndroid()) { // In Android we need the mimetype to open it. const mimetype = await this.ignoreErrors(this.getMimeTypeFromUrl(url)); @@ -1823,7 +1822,7 @@ export class CoreUtilsProvider { shouldOpenWithDialog(options: CoreUtilsOpenFileOptions = {}): boolean { const openFileAction = options.iOSOpenFileAction ?? CoreConstants.CONFIG.iOSDefaultOpenFileAction; - return CoreApp.isIOS() && openFileAction == OpenFileAction.OPEN_WITH; + return CorePlatform.isIOS() && openFileAction == OpenFileAction.OPEN_WITH; } } diff --git a/src/core/singletons/components-registry.ts b/src/core/singletons/components-registry.ts index d79e9ca9e..cd123802f 100644 --- a/src/core/singletons/components-registry.ts +++ b/src/core/singletons/components-registry.ts @@ -13,12 +13,14 @@ // limitations under the License. import { Component } from '@angular/core'; -import { AsyncComponent } from '@classes/async-component'; +import { AsyncDirective } from '@classes/async-directive'; import { CoreUtils } from '@services/utils/utils'; import { CoreLogger } from './logger'; /** * Registry to keep track of component instances. + * + * @deprecated since 4.1.1. Use CoreDirectivesRegistry instead. */ export class CoreComponentsRegistry { @@ -74,7 +76,7 @@ export class CoreComponentsRegistry { * @param componentClass Component class. * @returns Promise resolved when done. */ - static async waitComponentReady( + static async waitComponentReady( element: Element | null, componentClass?: ComponentConstructor, ): Promise { @@ -96,7 +98,7 @@ export class CoreComponentsRegistry { * @param componentClass Component class. * @returns Promise resolved when done. */ - static async waitComponentsReady( + static async waitComponentsReady( element: Element, selector: string, componentClass?: ComponentConstructor, diff --git a/src/core/singletons/directives-registry.ts b/src/core/singletons/directives-registry.ts new file mode 100644 index 000000000..a1b17e5b1 --- /dev/null +++ b/src/core/singletons/directives-registry.ts @@ -0,0 +1,145 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Directive } from '@angular/core'; +import { AsyncDirective } from '@classes/async-directive'; +import { CoreUtils } from '@services/utils/utils'; +import { CoreLogger } from './logger'; + +/** + * Registry to keep track of directive instances. + */ +export class CoreDirectivesRegistry { + + private static instances: WeakMap = new WeakMap(); + protected static logger = CoreLogger.getInstance('CoreDirectivesRegistry'); + + /** + * Register a directive instance. + * + * @param element Root element. + * @param instance Directive instance. + */ + static register(element: Element, instance: unknown): void { + const list = this.instances.get(element) ?? []; + list.push(instance); + this.instances.set(element, list); + } + + /** + * Resolve a directive instance. + * + * @param element Root element. + * @param directiveClass Directive class. + * @returns Directive instance. + */ + static resolve(element?: Element | null, directiveClass?: DirectiveConstructor): T | null { + const list = (element && this.instances.get(element) as T[]) ?? []; + + return list.find(instance => !directiveClass || instance instanceof directiveClass) ?? null; + } + + /** + * Resolve all directive instances. + * + * @param element Root element. + * @param directiveClass Directive class. + * @returns Directive instances. + */ + static resolveAll(element?: Element | null, directiveClass?: DirectiveConstructor): T[] { + const list = (element && this.instances.get(element) as T[]) ?? []; + + return list.filter(instance => !directiveClass || instance instanceof directiveClass) ?? []; + } + + /** + * Get a directive instance and fail if it cannot be resolved. + * + * @param element Root element. + * @param directiveClass Directive class. + * @returns Directive instance. + */ + static require(element: Element, directiveClass?: DirectiveConstructor): T { + const instance = this.resolve(element, directiveClass); + + if (!instance) { + throw new Error('Couldn\'t resolve directive instance'); + } + + return instance; + } + + /** + * Get a directive instance and wait to be ready. + * + * @param element Root element. + * @param directiveClass Directive class. + * @returns Promise resolved when done. + */ + static async waitDirectiveReady( + element: Element | null, + directiveClass?: DirectiveConstructor, + ): Promise { + const instance = this.resolve(element, directiveClass); + if (!instance) { + this.logger.error('No instance registered for element ' + directiveClass, element); + + return; + } + + await instance.ready(); + } + + /** + * Get all directive instances and wait to be ready. + * + * @param element Root element. + * @param directiveClass Directive class. + * @returns Promise resolved when done. + */ + static async waitDirectivesReady( + element: Element, + selector?: string, + directiveClass?: DirectiveConstructor, + ): Promise { + let elements: Element[] = []; + + if (!selector || element.matches(selector)) { + // Element to wait is myself. + elements = [element]; + } else { + elements = Array.from(element.querySelectorAll(selector)); + } + + if (!elements.length) { + return; + } + + await Promise.all(elements.map(async element => { + const instances = this.resolveAll(element, directiveClass); + + await Promise.all(instances.map(instance => instance.ready())); + })); + + // Wait for next tick to ensure directives are completely rendered. + await CoreUtils.nextTick(); + } + +} + +/** + * Directive constructor. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type DirectiveConstructor = { new(...args: any[]): T }; diff --git a/src/core/singletons/dom.ts b/src/core/singletons/dom.ts index 6e6b3d8d8..9f74b584b 100644 --- a/src/core/singletons/dom.ts +++ b/src/core/singletons/dom.ts @@ -585,3 +585,11 @@ export type CoreScrollOptions = { addYAxis?: number; addXAxis?: number; }; + +/** + * Source of a media element. + */ +export type CoreMediaSource = { + src: string; + type?: string; +}; diff --git a/src/core/singletons/events.ts b/src/core/singletons/events.ts index fb1a9c33a..10be74a3e 100644 --- a/src/core/singletons/events.ts +++ b/src/core/singletons/events.ts @@ -20,6 +20,7 @@ import { CoreFilepoolComponentFileEventData } from '@services/filepool'; import { CoreRedirectPayload } from '@services/navigator'; import { CoreCourseModuleCompletionData } from '@features/course/services/course-helper'; import { CoreScreenOrientation } from '@services/screen'; +import { VideoJSPlayer } from 'video.js'; /** * Observer instance to stop listening to an event. @@ -64,6 +65,7 @@ export interface CoreEventsData { [CoreEvents.ORIENTATION_CHANGE]: CoreEventOrientationData; [CoreEvents.COURSE_MODULE_VIEWED]: CoreEventCourseModuleViewed; [CoreEvents.COMPLETE_REQUIRED_PROFILE_DATA_FINISHED]: CoreEventCompleteRequiredProfileDataFinished; + [CoreEvents.JS_PLAYER_CREATED]: CoreEventJSVideoPlayerCreated; } /* @@ -108,7 +110,7 @@ export class CoreEvents { static readonly FILE_SHARED = 'file_shared'; static readonly KEYBOARD_CHANGE = 'keyboard_change'; /** - * @deprecated since app 4.0. Use CoreComponentsRegistry promises instead. + * @deprecated since app 4.0. Use CoreDirectivesRegistry promises instead. */ static readonly CORE_LOADING_CHANGED = 'core_loading_changed'; static readonly ORIENTATION_CHANGE = 'orientation_change'; @@ -123,6 +125,7 @@ export class CoreEvents { static readonly COMPLETE_REQUIRED_PROFILE_DATA_FINISHED = 'complete_required_profile_data_finished'; static readonly MAIN_HOME_LOADED = 'main_home_loaded'; static readonly FULL_SCREEN_CHANGED = 'full_screen_changed'; + static readonly JS_PLAYER_CREATED = 'js_player_created'; protected static logger = CoreLogger.getInstance('CoreEvents'); protected static observables: { [eventName: string]: Subject } = {}; @@ -490,3 +493,12 @@ export type CoreEventCourseModuleViewed = { export type CoreEventCompleteRequiredProfileDataFinished = { path: string; }; + +/** + * Data passed to JS_PLAYER_CREATED event. + */ +export type CoreEventJSVideoPlayerCreated = { + id: string; + element: HTMLAudioElement | HTMLVideoElement; + player: VideoJSPlayer; +}; diff --git a/src/core/singletons/media.ts b/src/core/singletons/media.ts new file mode 100644 index 000000000..cdf487cac --- /dev/null +++ b/src/core/singletons/media.ts @@ -0,0 +1,104 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { CorePlatform } from '@services/platform'; +import { CoreMimetypeUtils } from '@services/utils/mimetype'; + +/** + * Singleton with helper functions for media. + */ +export class CoreMedia { + + // Avoid creating singleton instances. + private constructor() { + // Nothing to do. + } + + /** + * Get all source URLs and types for a video or audio. + * + * @param mediaElement Audio or video element. + * @returns List of sources. + */ + static getMediaSources(mediaElement: HTMLVideoElement | HTMLAudioElement): CoreMediaSource[] { + const sources = Array.from(mediaElement.querySelectorAll('source')).map(source => ({ + src: source.src || source.getAttribute('target-src') || '', + type: source.type, + })); + + if (mediaElement.src) { + sources.push({ + src: mediaElement.src, + type: '', + }); + } + + return sources; + } + + /** + * Check if a source needs to be converted to be able to reproduce it. + * + * @param source Source. + * @returns Whether needs conversion. + */ + static sourceNeedsConversion(source: CoreMediaSource): boolean { + if (!CorePlatform.isIOS()) { + return false; + } + + let extension = source.type ? CoreMimetypeUtils.getExtension(source.type) : undefined; + if (!extension) { + extension = CoreMimetypeUtils.guessExtensionFromUrl(source.src); + } + + return !!extension && ['ogv', 'webm', 'oga', 'ogg'].includes(extension); + } + + /** + * Check if JS player should be used for a certain source. + * + * @param source Source. + * @returns Whether JS player should be used. + */ + static sourceUsesJavascriptPlayer(source: CoreMediaSource): boolean { + // For now, only use JS player if the source needs to be converted. + return CoreMedia.sourceNeedsConversion(source); + } + + /** + * Check if JS player should be used for a certain audio or video. + * + * @param mediaElement Media element. + * @returns Whether JS player should be used. + */ + static mediaUsesJavascriptPlayer(mediaElement: HTMLVideoElement | HTMLAudioElement): boolean { + if (!CorePlatform.isIOS()) { + return false; + } + + const sources = CoreMedia.getMediaSources(mediaElement); + + return sources.some(source => CoreMedia.sourceUsesJavascriptPlayer(source)); + } + +} + +/** + * Source of a media element. + */ +export type CoreMediaSource = { + src: string; + type?: string; +}; diff --git a/src/core/singletons/tests/components-registry.test.ts b/src/core/singletons/tests/components-registry.test.ts deleted file mode 100644 index 4cdeb231a..000000000 --- a/src/core/singletons/tests/components-registry.test.ts +++ /dev/null @@ -1,104 +0,0 @@ -// (C) Copyright 2015 Moodle Pty Ltd. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { wait } from '@/testing/utils'; -import { CoreComponentsRegistry } from '@singletons/components-registry'; - -const cssClassName = 'core-components-registry-test'; -const createAndRegisterInstance = () => { - const element = document.createElement('div'); - element.classList.add(cssClassName); - const instance = new ComponentsRegistryTestClass(); - - CoreComponentsRegistry.register(element, instance); - - return { element, instance }; -}; - -describe('CoreComponentsRegistry singleton', () => { - - let element: HTMLElement; - let testClassInstance: ComponentsRegistryTestClass; - - beforeEach(() => { - const result = createAndRegisterInstance(); - element = result.element; - testClassInstance = result.instance; - }); - - it('resolves stored instances', () => { - expect(CoreComponentsRegistry.resolve(element)).toEqual(testClassInstance); - expect(CoreComponentsRegistry.resolve(element, ComponentsRegistryTestClass)).toEqual(testClassInstance); - expect(CoreComponentsRegistry.resolve(element, CoreComponentsRegistry)).toEqual(null); - expect(CoreComponentsRegistry.resolve(document.createElement('div'))).toEqual(null); - }); - - it('requires stored instances', () => { - expect(CoreComponentsRegistry.require(element)).toEqual(testClassInstance); - expect(CoreComponentsRegistry.require(element, ComponentsRegistryTestClass)).toEqual(testClassInstance); - expect(() => CoreComponentsRegistry.require(element, CoreComponentsRegistry)).toThrow(); - expect(() => CoreComponentsRegistry.require(document.createElement('div'))).toThrow(); - }); - - it('waits for component ready', async () => { - expect(testClassInstance.isReady).toBe(false); - - await CoreComponentsRegistry.waitComponentReady(element); - - expect(testClassInstance.isReady).toBe(true); - }); - - it('waits for components ready: just one', async () => { - expect(testClassInstance.isReady).toBe(false); - - await CoreComponentsRegistry.waitComponentsReady(element, `.${cssClassName}`); - - expect(testClassInstance.isReady).toBe(true); - }); - - it('waits for components ready: multiple', async () => { - const secondResult = createAndRegisterInstance(); - const thirdResult = createAndRegisterInstance(); - thirdResult.element.classList.remove(cssClassName); // Remove the class so the element and instance aren't treated. - - const parent = document.createElement('div'); - parent.appendChild(element); - parent.appendChild(secondResult.element); - parent.appendChild(thirdResult.element); - - expect(testClassInstance.isReady).toBe(false); - expect(secondResult.instance.isReady).toBe(false); - expect(thirdResult.instance.isReady).toBe(false); - - await CoreComponentsRegistry.waitComponentsReady(parent, `.${cssClassName}`); - - expect(testClassInstance.isReady).toBe(true); - expect(secondResult.instance.isReady).toBe(true); - expect(thirdResult.instance.isReady).toBe(false); - }); - -}); - -class ComponentsRegistryTestClass { - - randomId = Math.random(); - isReady = false; - - async ready(): Promise { - await wait(50); - - this.isReady = true; - } - -} diff --git a/src/core/singletons/tests/directives-registry.test.ts b/src/core/singletons/tests/directives-registry.test.ts new file mode 100644 index 000000000..c0cbdb825 --- /dev/null +++ b/src/core/singletons/tests/directives-registry.test.ts @@ -0,0 +1,169 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { wait } from '@/testing/utils'; +import { CoreDirectivesRegistry } from '@singletons/directives-registry'; + +const cssClassName = 'core-directives-registry-test'; +const createAndRegisterInstance = (element?: HTMLElement) => { + element = element ?? document.createElement('div'); + element.classList.add(cssClassName); + const instance = new DirectivesRegistryTestClass(); + + CoreDirectivesRegistry.register(element, instance); + + return { element, instance }; +}; + +describe('CoreDirectivesRegistry singleton', () => { + + let element: HTMLElement; + let testClassInstance: DirectivesRegistryTestClass; + let testClassSecondInstance: DirectivesRegistryTestClass; + let testAltClassInstance: DirectivesRegistryAltTestClass; + + beforeEach(() => { + let result = createAndRegisterInstance(); + element = result.element; + testClassInstance = result.instance; + + result = createAndRegisterInstance(element); + testClassSecondInstance = result.instance; + + testAltClassInstance = new DirectivesRegistryAltTestClass(); + CoreDirectivesRegistry.register(element, testAltClassInstance); + }); + + it('resolves a stored instance', () => { + expect(CoreDirectivesRegistry.resolve(element)).toEqual(testClassInstance); + expect(CoreDirectivesRegistry.resolve(element, DirectivesRegistryTestClass)).toEqual(testClassInstance); + expect(CoreDirectivesRegistry.resolve(element, DirectivesRegistryAltTestClass)).toEqual(testAltClassInstance); + expect(CoreDirectivesRegistry.resolve(element, CoreDirectivesRegistry)).toEqual(null); + expect(CoreDirectivesRegistry.resolve(document.createElement('div'))).toEqual(null); + }); + + it('resolves all stored instances', () => { + expect(CoreDirectivesRegistry.resolveAll(element)).toEqual( + [testClassInstance, testClassSecondInstance, testAltClassInstance], + ); + expect(CoreDirectivesRegistry.resolveAll(element, DirectivesRegistryTestClass)).toEqual( + [testClassInstance, testClassSecondInstance], + ); + expect(CoreDirectivesRegistry.resolveAll(element, DirectivesRegistryAltTestClass)).toEqual([testAltClassInstance]); + expect(CoreDirectivesRegistry.resolveAll(element, CoreDirectivesRegistry)).toEqual([]); + expect(CoreDirectivesRegistry.resolveAll(document.createElement('div'))).toEqual([]); + }); + + it('requires a stored instance', () => { + expect(CoreDirectivesRegistry.require(element)).toEqual(testClassInstance); + expect(CoreDirectivesRegistry.require(element, DirectivesRegistryTestClass)).toEqual(testClassInstance); + expect(CoreDirectivesRegistry.require(element, DirectivesRegistryAltTestClass)).toEqual(testAltClassInstance); + expect(() => CoreDirectivesRegistry.require(element, CoreDirectivesRegistry)).toThrow(); + expect(() => CoreDirectivesRegistry.require(document.createElement('div'))).toThrow(); + }); + + it('waits for directive ready', async () => { + expect(testClassInstance.isReady).toBe(false); + + await CoreDirectivesRegistry.waitDirectiveReady(element); + + expect(testClassInstance.isReady).toBe(true); + }); + + it('waits for directives ready: just one element and directive', async () => { + const result = createAndRegisterInstance(); + expect(result.instance.isReady).toBe(false); + + await CoreDirectivesRegistry.waitDirectivesReady(result.element, `.${cssClassName}`); + + expect(result.instance.isReady).toBe(true); + expect(testClassInstance.isReady).toBe(false); + }); + + it('waits for directives ready: all directives, single element', async () => { + expect(testClassInstance.isReady).toBe(false); + expect(testClassSecondInstance.isReady).toBe(false); + expect(testAltClassInstance.isReady).toBe(false); + + await CoreDirectivesRegistry.waitDirectivesReady(element); + + expect(testClassInstance.isReady).toBe(true); + expect(testClassSecondInstance.isReady).toBe(true); + expect(testAltClassInstance.isReady).toBe(true); + }); + + it('waits for directives ready: filter by class, single element', async () => { + expect(testClassInstance.isReady).toBe(false); + expect(testClassSecondInstance.isReady).toBe(false); + expect(testAltClassInstance.isReady).toBe(false); + + await CoreDirectivesRegistry.waitDirectivesReady(element, `.${cssClassName}`, DirectivesRegistryTestClass); + + expect(testClassInstance.isReady).toBe(true); + expect(testClassSecondInstance.isReady).toBe(true); + expect(testAltClassInstance.isReady).toBe(false); + }); + + it('waits for directives ready: multiple elements', async () => { + const secondResult = createAndRegisterInstance(); + const thirdResult = createAndRegisterInstance(); + thirdResult.element.classList.remove(cssClassName); // Remove the class so the element and instance aren't treated. + + const parent = document.createElement('div'); + parent.appendChild(element); + parent.appendChild(secondResult.element); + parent.appendChild(thirdResult.element); + + expect(testClassInstance.isReady).toBe(false); + expect(testClassSecondInstance.isReady).toBe(false); + expect(testAltClassInstance.isReady).toBe(false); + expect(secondResult.instance.isReady).toBe(false); + expect(thirdResult.instance.isReady).toBe(false); + + await CoreDirectivesRegistry.waitDirectivesReady(parent, `.${cssClassName}`, DirectivesRegistryTestClass); + + expect(testClassInstance.isReady).toBe(true); + expect(testClassSecondInstance.isReady).toBe(true); + expect(testAltClassInstance.isReady).toBe(false); + expect(secondResult.instance.isReady).toBe(true); + expect(thirdResult.instance.isReady).toBe(false); + }); + +}); + +class DirectivesRegistryTestClass { + + randomId = Math.random(); + isReady = false; + + async ready(): Promise { + await wait(50); + + this.isReady = true; + } + +} + +class DirectivesRegistryAltTestClass { + + randomId = Math.random(); + isReady = false; + + async ready(): Promise { + await wait(50); + + this.isReady = true; + } + +} diff --git a/src/index.html b/src/index.html index 864f4b89a..817533811 100644 --- a/src/index.html +++ b/src/index.html @@ -16,6 +16,7 @@ + diff --git a/src/testing/services/behat-runtime.ts b/src/testing/services/behat-runtime.ts index 0f7eb5052..4970daaed 100644 --- a/src/testing/services/behat-runtime.ts +++ b/src/testing/services/behat-runtime.ts @@ -23,7 +23,7 @@ import { CoreNetwork, CoreNetworkService } from '@services/network'; import { CorePushNotifications, CorePushNotificationsProvider } from '@features/pushnotifications/services/pushnotifications'; import { CoreCronDelegate, CoreCronDelegateService } from '@services/cron'; import { CoreLoadingComponent } from '@components/loading/loading'; -import { CoreComponentsRegistry } from '@singletons/components-registry'; +import { CoreDirectivesRegistry } from '@singletons/directives-registry'; import { CoreDom } from '@singletons/dom'; import { Injectable } from '@angular/core'; import { CoreSites, CoreSitesProvider } from '@services/sites'; @@ -127,7 +127,7 @@ export class TestingBehatRuntimeService { .filter((element) => CoreDom.isElementVisible(element)); await Promise.all(elements.map(element => - CoreComponentsRegistry.waitComponentReady(element, CoreLoadingComponent))); + CoreDirectivesRegistry.waitDirectiveReady(element, CoreLoadingComponent))); }); } diff --git a/src/testing/utils.ts b/src/testing/utils.ts index e790fff28..2a54ef674 100644 --- a/src/testing/utils.ts +++ b/src/testing/utils.ts @@ -62,6 +62,8 @@ const DEFAULT_SERVICE_SINGLETON_MOCKS: [CoreSingletonProxy, unknown][] = [ [CorePlatform, mock({ is: () => false, isMobile: () => false, + isAndroid: () => false, + isIOS: () => false, ready: () => Promise.resolve(), resume: new Subject(), })], diff --git a/src/theme/components/videojs.scss b/src/theme/components/videojs.scss new file mode 100644 index 000000000..13dd8f6a8 --- /dev/null +++ b/src/theme/components/videojs.scss @@ -0,0 +1,93 @@ +/** + * VideoJS modifications. + **/ + +/** + * App specific modifications. + **/ + +// In case of error reset height to the default (otherwise no aspect ratio is available and height becomes 0). +.video-js.vjs-error { + height: 150px !important; +} + +// Fake full screen mode. +.vjs-ios-moodleapp-fs { + z-index: 100000 !important; + + canvas { + max-width: 100%; + max-height: 100%; + left: 0px; + right: 0px; + margin-left: auto; + margin-right: auto; + padding-bottom: var(--ion-safe-area-bottom, 0px); + } + + .vjs-control-bar { + height: calc(3em + var(--ion-safe-area-bottom, 0px)); + + .vjs-progress-control { + padding-bottom: var(--ion-safe-area-bottom, 0px); + } + } +} + +/** + * Styles extracted from: + * https://github.com/moodle/moodle/blob/3c5423d8c0bae003e6eb20119ca657c0c6760309/media/player/videojs/styles.css#L1773 + **/ + +/* Audio: Remove big play button (leave only the button in controls). */ +.video-js.vjs-audio .vjs-big-play-button { + display: none; +} +/* Audio: Make the controlbar visible by default */ +.video-js.vjs-audio .vjs-control-bar { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; +} +/* Make player height minimum to the controls height so when we hide video/poster area the controls are displayed correctly. */ +.video-js.vjs-audio { + min-height: 3em; +} +/* In case of error reset height to the default (otherwise no aspect ratio is available and height becomes 0). */ +.video-js.vjs-error { + height: 150px; +} +/* Minimum height for videos should not be less than the size of play button. */ +.mediaplugin_videojs video { + min-height: 32px; +} + +/* MDL-61020: VideoJS timeline progress bar should not be flipped in RTL mode. */ + +/* Prevent the progress bar from being flipped in RTL. */ +/*rtl:ignore*/ +.video-js .vjs-progress-holder .vjs-play-progress, +.video-js .vjs-progress-holder .vjs-load-progress, +.video-js .vjs-progress-holder .vjs-load-progress div { + left: 0; + right: auto; +} +/* Keep the video scrubber button at the end of the progress bar in RTL. */ +/*rtl:ignore*/ +.video-js .vjs-play-progress:before { + left: auto; + right: -0.5em; +} +/* Prevent the volume slider from being flipped in RTL. */ +/*rtl:ignore*/ +.video-js .vjs-volume-level { + left: 0; + right: auto; +} +/* Keep the volume slider handle at the end of the volume slider in RTL. */ +/*rtl:ignore*/ +.vjs-slider-horizontal .vjs-volume-level:before { + left: auto; + right: -0.5em; +} diff --git a/src/theme/theme.scss b/src/theme/theme.scss index c13ffdfd5..5baa55e42 100644 --- a/src/theme/theme.scss +++ b/src/theme/theme.scss @@ -26,6 +26,7 @@ @import "./components/rubrics.scss"; @import "./components/mod-label.scss"; @import "../core/components/error-info/error-info.scss"; +@import "./components/videojs.scss"; /* Some styles from 3rd party libraries. */ @import "./bootstrap.scss"; diff --git a/src/types/videojs.d.ts b/src/types/videojs.d.ts new file mode 100644 index 000000000..53ab829c2 --- /dev/null +++ b/src/types/videojs.d.ts @@ -0,0 +1,115 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +declare module 'video.js' { + function videojs( + elementOrId: string | HTMLElement, + options?: VideoJSOptions, + readyCallback?: () => void, + ): VideoJSPlayer; + + namespace videojs { + function getPlayer(id: string): VideoJSPlayer | null; + function log(...args): void; + function getComponent(name: string): any; // eslint-disable-line @typescript-eslint/no-explicit-any + } + + export default videojs; + + export type VideoJSPlayer = { + play: () => Promise; + pause: () => Promise; + on: (name: string, callback: (ev: Event) => void) => void; + off: (name: string, callback: (ev: Event) => void) => void; + dispose: () => void; + el: () => HTMLElement; + fluid: (val?: boolean) => void | boolean; + isFullscreen: (val?: boolean) => void | boolean; + videoHeight: () => number; + videoWidth: () => number; + currentDimensions: () => { width: number; height: number }; + dimension: (dimension: string, value: number) => void; + }; + + export type VideoJSOptions = { + aspectRatio?: string; + audioOnlyMode?: boolean; + audioPosterMode?: boolean; + autoplay?: boolean | string; + autoSetup?: boolean; + base?: string; + breakpoints?: Record; + children?: string[] | Record>; + controlBar?: { + fullscreenToggle?: boolean; + pictureInPictureToggle?: boolean; + remainingTimeDisplay?: { + displayNegative?: boolean; + }; + }; + controls?: boolean; + fluid?: boolean; + fullscreen?: { + options?: Record; + }; + height?: string | number; + id?: string; + inactivityTimeout?: number; + language?: string; + languages?: Record>; + liveui?: boolean; + liveTracker?: { + trackingThreshold?: number; + liveTolerance?: number; + }; + loop?: boolean; + muted?: boolean; + nativeControlsForTouch?: boolean; + normalizeAutoplay?: boolean; + notSupportedMessage?: string; + noUITitleAttributes?: boolean; + playbackRates?: number[]; + plugins?: Record>; + poster?: string; + preferFullWindow?: boolean; + preload?: PreloadOption; + responsive?: boolean; + restoreEl?: boolean | HTMLElement; + source?: TechSourceObject; + sources?: TechSourceObject[]; + src?: string; + suppressNotSupportedError?: boolean; + tag?: HTMLElement; + techCanOverridePoster?: boolean; + techOrder?: string[]; + userActions?: { + click?: boolean | ((ev: MouseEvent) => void); + doubleClick?: boolean | ((ev: MouseEvent) => void); + hotkeys?: boolean | ((ev: KeyboardEvent) => void) | { + fullscreenKey?: (ev: KeyboardEvent) => void; + muteKey?: (ev: KeyboardEvent) => void; + playPauseKey?: (ev: KeyboardEvent) => void; + }; + }; + 'vtt.js'?: string; + width?: string | number; + }; + + export type TechSourceObject = { + src: string; // Source URL. + type: string; // Mimetype. + }; + + export type PreloadOption = '' | 'none' | 'metadata' | 'auto'; +}