commit
						138377e504
					
				
							
								
								
									
										296
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										296
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -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", | ||||
|  | ||||
| @ -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": { | ||||
|  | ||||
| @ -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) { | ||||
|  | ||||
							
								
								
									
										748
									
								
								src/addons/filter/mediaplugin/classes/videojs-ogvjs.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										748
									
								
								src/addons/filter/mediaplugin/classes/videojs-ogvjs.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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 = <T>(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 (<HTMLVideoElement> this.el_).videoWidth ?? 0; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the video height. | ||||
|      * | ||||
|      * @returns Video heigth. | ||||
|      */ | ||||
|     videoHeight(): number { | ||||
|         return (<HTMLVideoElement> 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 { | ||||
|         (<HTMLVideoElement> this.el_).poster = url; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Is the media preloaded or not. | ||||
|      * | ||||
|      * @returns Whether it's preloaded. | ||||
|      */ | ||||
|     preload(): PreloadOption { | ||||
|         return <PreloadOption> 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; | ||||
| }; | ||||
| @ -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(); | ||||
|             }, | ||||
|         }, | ||||
|     ], | ||||
| }) | ||||
|  | ||||
| @ -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<HTMLVideoElement | HTMLAudioElement>('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<void> { | ||||
|         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<VideoJSOptions>(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 = <VideoDataSetup> CoreTextUtils.parseJSON(dataSetupString, {}); | ||||
|         const data = CoreTextUtils.parseJSON<VideoJSOptions>(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; | ||||
|     }[]; | ||||
| }; | ||||
|  | ||||
| @ -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; | ||||
|  | ||||
| @ -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<void> { | ||||
|         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, | ||||
|  | ||||
| @ -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<void> { | ||||
|         super.ngOnInit(); | ||||
| 
 | ||||
|         this.isIOS = CoreApp.isIOS(); | ||||
|         this.isIOS = CorePlatform.isIOS(); | ||||
|         this.isOnline = CoreNetwork.isOnline(); | ||||
| 
 | ||||
|         // Refresh online status when changes.
 | ||||
|  | ||||
| @ -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<void> { | ||||
|         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<HTMLElement>(this.selectors.dragHomes()))[0]; | ||||
| 
 | ||||
|  | ||||
| @ -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<void>; | ||||
| } | ||||
| @ -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.
 | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | ||||
| @ -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<VideoJSPlayer | null>(); | ||||
|     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<void> { | ||||
|         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<void> { | ||||
|         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<void> { | ||||
|         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', '')); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | ||||
| @ -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<AsyncComponent>(); | ||||
|     protected components = new Set<AsyncDirective>(); | ||||
|     protected loadedTimeout?: number; | ||||
|     protected hasChangesPromises: Promise<boolean>[] = []; | ||||
| 
 | ||||
| @ -66,7 +66,7 @@ export class PageLoadWatcher { | ||||
|      * | ||||
|      * @param component Component instance. | ||||
|      */ | ||||
|     async watchComponent(component: AsyncComponent): Promise<void> { | ||||
|     async watchComponent(component: AsyncDirective): Promise<void> { | ||||
|         this.components.add(component); | ||||
|         clearTimeout(this.loadedTimeout); | ||||
| 
 | ||||
|  | ||||
| @ -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); | ||||
|  | ||||
| @ -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<T extends CoreTabBase> implements OnInit, AfterViewInit, OnChanges, OnDestroy, AsyncComponent { | ||||
| export class CoreTabsBaseComponent<T extends CoreTabBase> implements OnInit, AfterViewInit, OnChanges, OnDestroy, AsyncDirective { | ||||
| 
 | ||||
|     // Minimum tab's width.
 | ||||
|     protected static readonly MIN_TAB_WIDTH = 107; | ||||
| @ -99,7 +99,7 @@ export class CoreTabsBaseComponent<T extends CoreTabBase> implements OnInit, Aft | ||||
| 
 | ||||
|         this.tabAction = new CoreTabsRoleTab(this); | ||||
| 
 | ||||
|         CoreComponentsRegistry.register(element.nativeElement, this); | ||||
|         CoreDirectivesRegistry.register(element.nativeElement, this); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
| @ -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); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
| @ -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'; | ||||
|  | ||||
| @ -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 { | ||||
|  | ||||
| @ -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'; | ||||
|  | ||||
| @ -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.
 | ||||
|  | ||||
| @ -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); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @ -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<CoreTabsOutle | ||||
|     protected showHideNavBarButtons(activatedPageName: string): void { | ||||
|         const elements = this.ionTabs.outlet.nativeEl.querySelectorAll('core-navbar-buttons'); | ||||
|         elements.forEach((element) => { | ||||
|             const instance = CoreComponentsRegistry.resolve(element, CoreNavBarButtonsComponent); | ||||
|             const instance = CoreDirectivesRegistry.resolve(element, CoreNavBarButtonsComponent); | ||||
| 
 | ||||
|             if (instance) { | ||||
|                 const pagetagName = element.closest('.ion-page')?.tagName; | ||||
|  | ||||
| @ -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); | ||||
|  | ||||
| @ -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 <core-format-text> children inside the element are done rendering. | ||||
|      */ | ||||
|     protected async waitFormatTextsRendered(): Promise<void> { | ||||
|         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), | ||||
|         ]); | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -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<void> { | ||||
|         await CoreComponentsRegistry.waitComponentsReady(element, 'core-format-text', CoreFormatTextDirective); | ||||
|         await CoreDirectivesRegistry.waitDirectivesReady(element, 'core-format-text', CoreFormatTextDirective); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
| @ -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 <core-format-text> children inside the element are done rendering. | ||||
|      */ | ||||
|     protected async waitFormatTextsRendered(): Promise<void> { | ||||
|         await CoreComponentsRegistry.waitComponentsReady(this.element, 'core-format-text', CoreFormatTextDirective); | ||||
|         await CoreDirectivesRegistry.waitDirectivesReady(this.element, 'core-format-text', CoreFormatTextDirective); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
| @ -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<void>(); | ||||
| 
 | ||||
|     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<void> { | ||||
|         return this.onReadyPromise; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | ||||
| @ -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(); | ||||
|  | ||||
| @ -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.
 | ||||
|  | ||||
| @ -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; | ||||
|  | ||||
| @ -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; | ||||
| 
 | ||||
|  | ||||
| @ -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); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
| @ -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; | ||||
|             } | ||||
|  | ||||
| @ -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'); | ||||
|             } | ||||
|  | ||||
| @ -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 { | ||||
|  | ||||
| @ -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); | ||||
| 
 | ||||
|  | ||||
| @ -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 { | ||||
|  | ||||
| @ -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) => { | ||||
|  | ||||
| @ -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(); | ||||
|         } | ||||
| 
 | ||||
|  | ||||
| @ -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) => { | ||||
|  | ||||
| @ -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'; | ||||
|  | ||||
| @ -228,7 +228,7 @@ export class CorePushNotificationsProvider { | ||||
|      * @returns Promise resolved when done. | ||||
|      */ | ||||
|     protected async createDefaultChannel(): Promise<void> { | ||||
|         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)) { | ||||
|  | ||||
| @ -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<void> { | ||||
|  | ||||
| @ -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<Cor | ||||
|             }, | ||||
|         ]; | ||||
| 
 | ||||
|         if (CoreApp.isIOS()) { | ||||
|         if (CorePlatform.isIOS()) { | ||||
|             sections.push({ | ||||
|                 name: 'core.sharedfiles.sharedfiles', | ||||
|                 path: SHAREDFILES_PAGE_NAME + '/list/root', | ||||
|  | ||||
| @ -12,7 +12,6 @@ | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { CoreApp } from '@services/app'; | ||||
| import { Component, OnDestroy } from '@angular/core'; | ||||
| import { CoreConstants } from '@/core/constants'; | ||||
| import { CoreLocalNotifications } from '@services/local-notifications'; | ||||
| @ -111,10 +110,10 @@ export class CoreSettingsDeviceInfoPage implements OnDestroy { | ||||
| 
 | ||||
|         if (CorePlatform.isMobile()) { | ||||
|             this.deviceInfo.deviceType = CorePlatform.is('tablet') ? 'tablet' : 'phone'; | ||||
|             if (CoreApp.isAndroid()) { | ||||
|             if (CorePlatform.isAndroid()) { | ||||
|                 this.deviceInfo.deviceOs = 'android'; | ||||
|                 this.deviceOsTranslated = 'Android'; | ||||
|             } else if (CoreApp.isIOS()) { | ||||
|             } else if (CorePlatform.isIOS()) { | ||||
|                 this.deviceInfo.deviceOs = 'ios'; | ||||
|                 this.deviceOsTranslated = 'iOS'; | ||||
|             } else { | ||||
|  | ||||
| @ -20,13 +20,13 @@ import { CoreLang } from '@services/lang'; | ||||
| import { CoreDomUtils } from '@services/utils/dom'; | ||||
| import { CorePushNotifications } from '@features/pushnotifications/services/pushnotifications'; | ||||
| import { CoreSettingsHelper, CoreColorScheme, CoreZoomLevel } from '../../services/settings-helper'; | ||||
| import { CoreApp } from '@services/app'; | ||||
| import { CoreIframeUtils } from '@services/utils/iframe'; | ||||
| import { Diagnostic, Translate } from '@singletons'; | ||||
| import { CoreSites } from '@services/sites'; | ||||
| import { CoreUtils } from '@services/utils/utils'; | ||||
| import { AlertButton } from '@ionic/angular'; | ||||
| import { CoreNavigator } from '@services/navigator'; | ||||
| import { CorePlatform } from '@services/platform'; | ||||
| 
 | ||||
| /** | ||||
|  * Page that displays the general settings. | ||||
| @ -81,7 +81,7 @@ export class CoreSettingsGeneralPage { | ||||
|                 this.colorSchemes.push(CoreColorScheme.LIGHT); | ||||
|                 this.selectedScheme = this.colorSchemes[0]; | ||||
|             } else { | ||||
|                 this.isAndroid = CoreApp.isAndroid(); | ||||
|                 this.isAndroid = CorePlatform.isAndroid(); | ||||
|                 this.colorSchemes = CoreSettingsHelper.getAllowedColorSchemes(); | ||||
|                 this.selectedScheme = await CoreConfig.get(CoreConstants.SETTINGS_COLOR_SCHEME, CoreColorScheme.LIGHT); | ||||
|             } | ||||
|  | ||||
| @ -15,7 +15,7 @@ | ||||
| import { Injectable } from '@angular/core'; | ||||
| import { CoreSettingsHandler, CoreSettingsHandlerData } from '@features/settings/services/settings-delegate'; | ||||
| import { SHAREDFILES_PAGE_NAME } from '@features/sharedfiles/sharedfiles.module'; | ||||
| import { CoreApp } from '@services/app'; | ||||
| import { CorePlatform } from '@services/platform'; | ||||
| import { makeSingleton } from '@singletons'; | ||||
| 
 | ||||
| /** | ||||
| @ -33,7 +33,7 @@ export class CoreSharedFilesSettingsHandlerService implements CoreSettingsHandle | ||||
|      * @returns Whether or not the handler is enabled on a site level. | ||||
|      */ | ||||
|     async isEnabled(): Promise<boolean> { | ||||
|         return CoreApp.isIOS(); | ||||
|         return CorePlatform.isIOS(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
| @ -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<boolean> { | ||||
|         return CoreApp.isIOS(); | ||||
|         return CorePlatform.isIOS(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
| @ -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; | ||||
|         } | ||||
| 
 | ||||
|  | ||||
| @ -97,7 +97,7 @@ export class CoreSitePluginsProvider { | ||||
|         }; | ||||
| 
 | ||||
|         if (args.appismobile) { | ||||
|             defaultArgs.appplatform = CoreApp.isIOS() ? 'ios' : 'android'; | ||||
|             defaultArgs.appplatform = CorePlatform.isIOS() ? 'ios' : 'android'; | ||||
|         } | ||||
| 
 | ||||
|         return { | ||||
|  | ||||
| @ -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<HTMLElement>) { | ||||
|         this.element = element; | ||||
| 
 | ||||
|         CoreComponentsRegistry.register(element, this); | ||||
|         CoreDirectivesRegistry.register(element, this); | ||||
| 
 | ||||
|         this.element.addEventListener('click', (event) => | ||||
|             this.dismissOnBackOrBackdrop(event.target as HTMLElement)); | ||||
|  | ||||
| @ -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); | ||||
|     } | ||||
|  | ||||
| @ -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<void> { | ||||
|     await CorePlatform.ready(); | ||||
| 
 | ||||
|     if (!CoreApp.isIOS() || !('WKUserScript' in window)) { | ||||
|     if (!CorePlatform.isIOS() || !('WKUserScript' in window)) { | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -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; | ||||
|         } | ||||
| 
 | ||||
|  | ||||
| @ -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(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @ -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; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
| @ -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<number> { | ||||
|         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<string> { | ||||
|         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://'); | ||||
|         } | ||||
| 
 | ||||
|  | ||||
| @ -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<void> { | ||||
|         if (!CoreApp.isIOS()) { | ||||
|         if (!CorePlatform.isIOS()) { | ||||
|             await Diagnostic.requestLocationAuthorization(); | ||||
| 
 | ||||
|             return; | ||||
|  | ||||
| @ -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<void> { | ||||
|         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; | ||||
|  | ||||
| @ -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). | ||||
|      * | ||||
|  | ||||
| @ -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', () => { | ||||
|  | ||||
| @ -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<number | null> => { | ||||
|             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<T = unknown>(element: Element): T | undefined { | ||||
|         return CoreComponentsRegistry.resolve<T>(element) ?? undefined; | ||||
|         return CoreDirectivesRegistry.resolve<T>(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<void> { | ||||
|         if (!CoreApp.isIOS()) { | ||||
|         if (!CorePlatform.isIOS()) { | ||||
|             return; // Only wait in iOS.
 | ||||
|         } | ||||
| 
 | ||||
|  | ||||
| @ -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<void> { | ||||
|         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; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
| @ -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); | ||||
|             } | ||||
|  | ||||
| @ -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)); | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -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 }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
| @ -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<void> { | ||||
|         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; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | ||||
| @ -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<T extends AsyncComponent>( | ||||
|     static async waitComponentReady<T extends AsyncDirective>( | ||||
|         element: Element | null, | ||||
|         componentClass?: ComponentConstructor<T>, | ||||
|     ): Promise<void> { | ||||
| @ -96,7 +98,7 @@ export class CoreComponentsRegistry { | ||||
|      * @param componentClass Component class. | ||||
|      * @returns Promise resolved when done. | ||||
|      */ | ||||
|     static async waitComponentsReady<T extends AsyncComponent>( | ||||
|     static async waitComponentsReady<T extends AsyncDirective>( | ||||
|         element: Element, | ||||
|         selector: string, | ||||
|         componentClass?: ComponentConstructor<T>, | ||||
|  | ||||
							
								
								
									
										145
									
								
								src/core/singletons/directives-registry.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										145
									
								
								src/core/singletons/directives-registry.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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<Element, unknown[]> = 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<T>(element?: Element | null, directiveClass?: DirectiveConstructor<T>): 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<T>(element?: Element | null, directiveClass?: DirectiveConstructor<T>): 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<T>(element: Element, directiveClass?: DirectiveConstructor<T>): 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<T extends AsyncDirective>( | ||||
|         element: Element | null, | ||||
|         directiveClass?: DirectiveConstructor<T>, | ||||
|     ): Promise<void> { | ||||
|         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<T extends AsyncDirective>( | ||||
|         element: Element, | ||||
|         selector?: string, | ||||
|         directiveClass?: DirectiveConstructor<T>, | ||||
|     ): Promise<void> { | ||||
|         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<T>(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<T = Directive> = { new(...args: any[]): T }; | ||||
| @ -585,3 +585,11 @@ export type CoreScrollOptions = { | ||||
|     addYAxis?: number; | ||||
|     addXAxis?: number; | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Source of a media element. | ||||
|  */ | ||||
| export type CoreMediaSource = { | ||||
|     src: string; | ||||
|     type?: string; | ||||
| }; | ||||
|  | ||||
| @ -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<unknown> } = {}; | ||||
| @ -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; | ||||
| }; | ||||
|  | ||||
							
								
								
									
										104
									
								
								src/core/singletons/media.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										104
									
								
								src/core/singletons/media.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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; | ||||
| }; | ||||
| @ -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<void> { | ||||
|         await wait(50); | ||||
| 
 | ||||
|         this.isReady = true; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										169
									
								
								src/core/singletons/tests/directives-registry.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										169
									
								
								src/core/singletons/tests/directives-registry.test.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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<void> { | ||||
|         await wait(50); | ||||
| 
 | ||||
|         this.isReady = true; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| class DirectivesRegistryAltTestClass { | ||||
| 
 | ||||
|     randomId = Math.random(); | ||||
|     isReady = false; | ||||
| 
 | ||||
|     async ready(): Promise<void> { | ||||
|         await wait(50); | ||||
| 
 | ||||
|         this.isReady = true; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| @ -16,6 +16,7 @@ | ||||
|     <meta name="msapplication-tap-highlight" content="no" /> | ||||
| 
 | ||||
|     <link rel="icon" type="image/png" href="assets/icon/favicon.png" /> | ||||
|     <link rel="stylesheet" href="assets/lib/video.js/video-js.min.css"> | ||||
| 
 | ||||
|     <!-- add to homescreen for ios --> | ||||
|     <meta name="apple-mobile-web-app-capable" content="yes" /> | ||||
|  | ||||
| @ -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))); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -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<void>(), | ||||
|     })], | ||||
|  | ||||
							
								
								
									
										93
									
								
								src/theme/components/videojs.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								src/theme/components/videojs.scss
									
									
									
									
									
										Normal file
									
								
							| @ -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; | ||||
| } | ||||
| @ -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"; | ||||
|  | ||||
							
								
								
									
										115
									
								
								src/types/videojs.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										115
									
								
								src/types/videojs.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -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<void>; | ||||
|         pause: () => Promise<void>; | ||||
|         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<string, number>; | ||||
|         children?: string[] | Record<string, Record<string, unknown>>; | ||||
|         controlBar?: { | ||||
|             fullscreenToggle?: boolean; | ||||
|             pictureInPictureToggle?: boolean; | ||||
|             remainingTimeDisplay?: { | ||||
|                 displayNegative?: boolean; | ||||
|             }; | ||||
|         }; | ||||
|         controls?: boolean; | ||||
|         fluid?: boolean; | ||||
|         fullscreen?: { | ||||
|             options?: Record<string, unknown>; | ||||
|         }; | ||||
|         height?: string | number; | ||||
|         id?: string; | ||||
|         inactivityTimeout?: number; | ||||
|         language?: string; | ||||
|         languages?: Record<string, Record<string, string>>; | ||||
|         liveui?: boolean; | ||||
|         liveTracker?: { | ||||
|             trackingThreshold?: number; | ||||
|             liveTolerance?: number; | ||||
|         }; | ||||
|         loop?: boolean; | ||||
|         muted?: boolean; | ||||
|         nativeControlsForTouch?: boolean; | ||||
|         normalizeAutoplay?: boolean; | ||||
|         notSupportedMessage?: string; | ||||
|         noUITitleAttributes?: boolean; | ||||
|         playbackRates?: number[]; | ||||
|         plugins?: Record<string, Record<string, unknown>>; | ||||
|         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'; | ||||
| } | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user