Compare commits
614 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 78395e3ea0 | |||
| e269ff3d79 | |||
| be495b1c56 | |||
| 3f032619b4 | |||
| ef5ba706b7 | |||
| 3b91c2224c | |||
| 822d069b82 | |||
| 8af9f2cedf | |||
| 3d8d8eb199 | |||
| a0d599ce2f | |||
| fd7b5ec04e | |||
| c9818c92b3 | |||
| f57163f2bb | |||
| dbdcb4e367 | |||
| e1dd0f9c04 | |||
| 2cb6a88c5e | |||
| eb89ba03db | |||
| cb02c8619d | |||
| 7d296e6a3d | |||
| 35bb5b477b | |||
| 925a9fdea3 | |||
| f0baa6ad67 | |||
| f25749470b | |||
| 8a4985e6a3 | |||
| 2cce60fb9f | |||
| 826acf9a3f | |||
| 186de15781 | |||
| 17dc69f87a | |||
| 8268c7a7af | |||
| a71b99ff10 | |||
| 54c4f49edb | |||
| 92cd1cf58b | |||
| 64570c71ce | |||
| cbc4b7877f | |||
| b62f6feaea | |||
| b6dec385dc | |||
| a12581ed0e | |||
| e651e51c50 | |||
| 56321e52fc | |||
| 73eec1f7ba | |||
| a0cddb256a | |||
| 14b3571af3 | |||
| af08c5094d | |||
| ef12dc8cc5 | |||
| fcfc0361dc | |||
| a8847bdeff | |||
| 3f5ab5ecc6 | |||
| 077c7cae15 | |||
| f916cffd41 | |||
| ea4fb95a3c | |||
| 3dbec85269 | |||
| 2a42c3e715 | |||
| c2a6a3f082 | |||
| 61972a7b2d | |||
| 4a562443b1 | |||
| 9515c1b01a | |||
| 126d891e5e | |||
| dad1df0244 | |||
| aff429e5ea | |||
| 3457c4fcdc | |||
| 74c0358d33 | |||
| 8735d3e81a | |||
| 4180d5b5b1 | |||
| ca751a660f | |||
| fb7f5cf1ba | |||
| fe2f52d76a | |||
| ee32ae93c3 | |||
| 877fc96da1 | |||
| 67a5b4e05a | |||
| d0a5083841 | |||
| 2d8c078a6f | |||
| f543dcff24 | |||
| 5cfa5b0add | |||
| 13feb1e822 | |||
| dc755dbe62 | |||
| b334707463 | |||
| ca093f5b34 | |||
| 2e4226b828 | |||
| 095a3daadd | |||
| baa7c79f6c | |||
| f5e308c5b4 | |||
| c0c29759fa | |||
| ddffbe92a0 | |||
| 888e48eb5b | |||
| 61e7351d0b | |||
| 4b0571e123 | |||
| bf7f707ab7 | |||
| accc320971 | |||
| 4276a9f03a | |||
| 7ae3fb6109 | |||
| 8c96e2fd90 | |||
| 30e5bce38f | |||
| 620028db6d | |||
| 850b5d6b25 | |||
| eaf003f5a7 | |||
| ab1efbe9cf | |||
| 14f8453067 | |||
| 090f112463 | |||
| 01d569f1b5 | |||
| 75670ef3f9 | |||
| f88f703e84 | |||
| a5fdef0196 | |||
| f10eee335d | |||
| 20856891b6 | |||
| 60dd1193ab | |||
| 2bc95af73c | |||
| 178bb65b4a | |||
| f7f046ea53 | |||
| ec0896e17a | |||
| a5f35248bd | |||
| a05eccce9e | |||
| e118777636 | |||
| 7998e8158e | |||
| 14f837d1f0 | |||
| cff1d3cf16 | |||
| b2cd96982e | |||
| a89eb8e857 | |||
| 7c1ccd2cc9 | |||
| e3a12bc601 | |||
| e20508956a | |||
| 4000d0e027 | |||
| 77fcced776 | |||
| e67b5a227f | |||
| 150a0a8a90 | |||
| 269dddc92e | |||
| 508ef5cbbb | |||
| 500d06204c | |||
| fe8436179f | |||
| adedfec1d0 | |||
| b974111eb4 | |||
| 3d7b50a2e3 | |||
| 32c11d1f61 | |||
| 5c48747d0f | |||
| 5a0abb3595 | |||
| 95b98c9053 | |||
| 518e09e3cb | |||
| ad632b0680 | |||
| 73aa925067 | |||
| 391b0ac7ab | |||
| ae2bb7afcd | |||
| 0681c3ae08 | |||
| 4aa96bb288 | |||
| b24739d70d | |||
| dffe8e055a | |||
| adadc8bb3e | |||
| 5f3562fe5b | |||
| 0d7c535a70 | |||
| 39f39f23a7 | |||
| ff0de6f2e7 | |||
| 8658b3c0d3 | |||
| 8551bc2dc1 | |||
| a0abf336cf | |||
| 7d71dd0507 | |||
| 54c190f86c | |||
| e198c69741 | |||
| 7c1d3e52e5 | |||
| 5460719297 | |||
| 83b770e4e8 | |||
| a5c9835d9d | |||
| f0840b78e1 | |||
| ab79f1e0e6 | |||
| 16a2f511bb | |||
| 39fdd72e9f | |||
| f93a7e284c | |||
| b5e480a486 | |||
| bf128babcc | |||
| 46d1ad221f | |||
| 7aacbec108 | |||
| f77506c454 | |||
| e3f1f21ce9 | |||
| 08c86615a0 | |||
| b0425b37c0 | |||
| d94be7aa5e | |||
| 40bdddd031 | |||
| da94dee780 | |||
| 587a21c5f6 | |||
| f994617d44 | |||
| a92f5fc2a1 | |||
| 208a360c47 | |||
| f728cbf817 | |||
| aef5665cb5 | |||
| 6ea47d26d0 | |||
| a7a36033a2 | |||
| 32f4d99c98 | |||
| 2d08a2f339 | |||
| 1f0ebff241 | |||
| 5c26e49c17 | |||
| ea85f2f5d6 | |||
| af9b123e8d | |||
| d322c0909a | |||
| 0bfe84d73d | |||
| 244e8227e7 | |||
| 5da8073235 | |||
| 5479b7ee95 | |||
| b4e932581c | |||
| 1f65cca002 | |||
| 1a9cdfa93b | |||
| d780a8cb12 | |||
| eaf619f4f8 | |||
| 3dd8584954 | |||
| ebdc26a00d | |||
| dd47dd09da | |||
| bc6d2ac549 | |||
| b2cd8197d4 | |||
| 23ea9e77c4 | |||
| 42ac51f1e9 | |||
| d433878680 | |||
| 11fa12001c | |||
| a82a6963e9 | |||
| a98a80001c | |||
| ad1e73d0cc | |||
| 55c0e7e8c5 | |||
| 3d35ae32c8 | |||
| 33e3bec337 | |||
| 9c12272846 | |||
| 0cc2249b97 | |||
| a5d0c0b6b7 | |||
| 3736a55bad | |||
| 303213da73 | |||
| 8d47eaf74c | |||
| 9f10c0dcf4 | |||
| d981e4c2c6 | |||
| 8f3d1bd091 | |||
| 3b9f557075 | |||
| 18cdec51d9 | |||
| 276a51b8c3 | |||
| 14e1d6aad3 | |||
| 9c65300fc4 | |||
| cf01dd9397 | |||
| 162318b439 | |||
| cd89cca984 | |||
| 25222c7310 | |||
| b2bfde4426 | |||
| 8602b53260 | |||
| 145b361581 | |||
| 7743dd8e7f | |||
| ed2886d449 | |||
| 002dfacc3e | |||
| fd9fbc32a8 | |||
| e54deeb4f7 | |||
| c85f211d5e | |||
| 67f848a9ee | |||
| fe336122e0 | |||
| 6d8d8493a6 | |||
| 5d7d249fff | |||
| 35ca030dfb | |||
| fab6fb0f5b | |||
| 3b8fc807f7 | |||
| 13cfb9ff14 | |||
| aabd5e75d8 | |||
| 7be6fb9501 | |||
| 9fc63e5cd3 | |||
| 7a1e8be48a | |||
| d299a65e03 | |||
| 2337f7a3ba | |||
| 4853d7e2fd | |||
| ca1a375f2d | |||
| 0d098815a2 | |||
| 30cbfb6ffd | |||
| 4c7deffdfe | |||
| 15133e561f | |||
| c235b52e3b | |||
| 903f356252 | |||
| 54c44b4379 | |||
| dcbeecc8cf | |||
| bf182c65ad | |||
| 7c5e4101b0 | |||
| 2736ac418f | |||
| e2dac10501 | |||
| c15d7ab28e | |||
| de78f8b1d5 | |||
| e56d32180c | |||
| 6c92837317 | |||
| d4015a86ef | |||
| 4b4be96e26 | |||
| 823e1d983c | |||
| 47fba5b81a | |||
| ee23ebb705 | |||
| d6d3a95daf | |||
| e839e632fc | |||
| cb6f0a9a32 | |||
| c5515934d9 | |||
| 7dafe85c53 | |||
| 3bc65c252d | |||
| d4f9ab9f37 | |||
| b96227b07a | |||
| da12ee452c | |||
| 99df043b5f | |||
| 54d53f1b91 | |||
| 3645b9ffcf | |||
| 9d0ce25be7 | |||
| 1c860f74fb | |||
| 75e31dc3a4 | |||
| 10a8452709 | |||
| f824f73491 | |||
| 16c34e3d52 | |||
| 76c4599dee | |||
| 1af80096c7 | |||
| cbb2bb4502 | |||
| 0f3bcaa8e6 | |||
| ae6575043e | |||
| 0dbd5b456c | |||
| 81f3e62446 | |||
| e0bb25466d | |||
| cb334aab0d | |||
| 9cef850d06 | |||
| 79ecc87db4 | |||
| 12644bfbea | |||
| bc6b58aa3f | |||
| abeded1d03 | |||
| a466f77b6e | |||
| 968ce5432a | |||
| 277506b3c8 | |||
| 1c48ea1908 | |||
| f2ac1557a4 | |||
| 7213c1e35f | |||
| 9f5f953b46 | |||
| 3ff6054c20 | |||
| 9fb25254a6 | |||
| 8c6a06d490 | |||
| dd37837848 | |||
| 5ca9325e16 | |||
| b8fdf0911a | |||
| a7302fee4b | |||
| 41bf4d2764 | |||
| 0970140a3a | |||
| b8eb8ec052 | |||
| 46b7d6a25a | |||
| aab2f5b014 | |||
| 6062094e6f | |||
| f7e36619df | |||
| 167f5173cd | |||
| c7ebd42fe9 | |||
| 94018c6ee3 | |||
| 388327ec1d | |||
| 0ba56c271d | |||
| bd1216c275 | |||
| 3180063fe3 | |||
| 9cb9a6607b | |||
| 42cdcd5c25 | |||
| 8b1da27af7 | |||
| 638e228ce9 | |||
| 94469ccd0e | |||
| 48f7e1f39c | |||
| 60f7abfa42 | |||
| cd391db302 | |||
| 8613d6971f | |||
| cc18153889 | |||
| be4bf9e2a3 | |||
| 085bd4aab9 | |||
| 977d103c3a | |||
| 0778f07922 | |||
| e12154601b | |||
| b22d50f760 | |||
| 302f21b477 | |||
| 65da515854 | |||
| e9bd3154a5 | |||
| 1da147e110 | |||
| e7dd85420c | |||
| 3e542871c6 | |||
| 26b78f1225 | |||
| 57261b5094 | |||
| c7f24133e5 | |||
| 87aa397a7c | |||
| e2a18ec765 | |||
| 26ee2ffbb6 | |||
| 38dd1ddbe8 | |||
| 11bc79520a | |||
| cc0ae60c83 | |||
| 30b028d836 | |||
| a30a70c6b5 | |||
| 9503003a4a | |||
| a58edad8ea | |||
| d0ba619b45 | |||
| 64bad6758b | |||
| b902b8f9ba | |||
| 7bb4ee96e7 | |||
| 6c3e960096 | |||
| cacb0f1ea0 | |||
| 5a2e33ccd2 | |||
| 5d389a0796 | |||
| 6db1d8af2a | |||
| 51d73e09ae | |||
| 3c324aa1b2 | |||
| a94092ac78 | |||
| f9d72d12b6 | |||
| 81e11c2ae9 | |||
| f7a82ccdf7 | |||
| 44e3395835 | |||
| d558717152 | |||
| b9b021f466 | |||
| 13bfd63dc2 | |||
| 9600c4f1c9 | |||
| de9696471a | |||
| c954f8b984 | |||
| 08479242f7 | |||
| d70783cc82 | |||
| a2f0617d5a | |||
| 27b6ca824d | |||
| e84f8ee541 | |||
| ad87dbdcb9 | |||
| 9c42c120f5 | |||
| 740f2e8f6f | |||
| a0388689cf | |||
| 46cb5e1078 | |||
| abc53cf00f | |||
| 68f367bd39 | |||
| d54097799c | |||
| ffc25520bb | |||
| ccdc68870a | |||
| 7820da181e | |||
| ec4d43d2ec | |||
| c12a04b55f | |||
| e0fe4c7103 | |||
| 82f2983ac2 | |||
| d2733cf486 | |||
| 3aabd298ec | |||
| a0e375d2dc | |||
| 8806c8ea0c | |||
| f0120010fe | |||
| a5a121ee00 | |||
| 459a0eaa91 | |||
| 84fefbaa14 | |||
| 023b8c370a | |||
| 0a023ac6f2 | |||
| 83f7e4498a | |||
| d57040fc5b | |||
| 7df62ba530 | |||
| f51ec50113 | |||
| 6748cbe52d | |||
| fda2dcc484 | |||
| 07a5f06695 | |||
| c9392911d9 | |||
| 88dec55116 | |||
| f30de3f4a0 | |||
| 3c5aafee2d | |||
| 52b4e48217 | |||
| 58ed3b1ca4 | |||
| bfef49bc33 | |||
| b7a61e5bc0 | |||
|
|
15a3ffd406 | ||
| 66f934e46e | |||
| 47b85cfebc | |||
| d0b060f31a | |||
| a756a9dc16 | |||
| 207b82cc8b | |||
| 26df9633b4 | |||
| 2b749d6a3b | |||
| 8c403f106c | |||
| 36aa226e9c | |||
|
|
1946b77285 | ||
| a09ea26b0e | |||
| 1556506bd7 | |||
| f94f6ec956 | |||
| aae566b82a | |||
| dd8aeefdfd | |||
| 7d951a5115 | |||
| 556722693c | |||
| 78f350b72b | |||
| 340ee05443 | |||
| 561a1aa5a5 | |||
| e2fa4f9cfc | |||
| ee56187c9d | |||
| d3dbceb749 | |||
| 799c953b30 | |||
| e766d6da69 | |||
| 53ed81fa81 | |||
| 320d35d9e5 | |||
| a1bfaaaf92 | |||
| 03ebc87aa5 | |||
| d2447d2d8a | |||
| 296f146835 | |||
| 9a7e38d897 | |||
| f0fcf996c9 | |||
| c1b14979a2 | |||
| 9e9fdfe37f | |||
| 135ce2dfdd | |||
| 167d46ca76 | |||
| 23d2c252fe | |||
| 9100c1d9de | |||
| b45b232ecf | |||
| 9648d30aa2 | |||
| 5a04c2eeee | |||
| 74fe85f3a2 | |||
| 4b44efe6fc | |||
|
|
8922054c44 | ||
| 302353f448 | |||
| 3891d946e0 | |||
| e92e2e22f5 | |||
| 4f4eb46a60 | |||
| 0daef615c1 | |||
| 6ce8bba6b7 | |||
| 09cf3ce54e | |||
| 872b0dea6e | |||
| 376f20aeab | |||
| 57aae96781 | |||
| 7c27c33aa0 | |||
| 7dc354509b | |||
| 7c09440084 | |||
|
|
5538ec4d3d | ||
| 89168b643c | |||
| 7fefe255ea | |||
| 3423aac49a | |||
|
|
cc8d1d8a94 | ||
|
|
c0a863740a | ||
|
|
f006eed113 | ||
| 3f131f44d1 | |||
| 5cfc089bf1 | |||
| 987b1ad655 | |||
| 1cd06040da | |||
| f99220bf8b | |||
| 8bef1d0c99 | |||
| 47a73cfa6c | |||
| 2dd901b1d5 | |||
| 46725c6fa4 | |||
|
|
adfc3fca5f | ||
|
|
077610820c | ||
|
|
9a3bd3ff88 | ||
| 85fed9d4c5 | |||
| 5a2d8d22cb | |||
| 761f2c21a8 | |||
| c34340d0d1 | |||
| 045cd5a528 | |||
| 27b0609d34 | |||
| 65556d9cad | |||
| 30835e94c0 | |||
| ee8ac1d9ee | |||
| 2be44d54ae | |||
| cf8bedfad9 | |||
| 5dc9b498d4 | |||
| fb7a168d31 | |||
| 484db037c0 | |||
| 15a89583a2 | |||
| 75cc0d5a87 | |||
| 0e3b962a00 | |||
| afaf65a53d | |||
| f00d3b5aaf | |||
| 03bd442783 | |||
| 787bd1a532 | |||
| bd5befc18c | |||
| 829047585a | |||
| 78da45d468 | |||
| 7f7ab7b752 | |||
| 79fb10c556 | |||
| ffd9c6fb7a | |||
| 4074774d3a | |||
| e7d43c566f | |||
| 52b94003bf | |||
| 13bd280062 | |||
| 209e4221d2 | |||
| c4832a9ef7 | |||
| 6cb8cc174b | |||
| e9b882376c | |||
| a4948d3eef | |||
| 746a78577e | |||
| 268ef7dae3 | |||
| 6c1d666fdf | |||
| 53fb3e8f46 | |||
| 2df0b1a0fb | |||
| 3846d1a926 | |||
| 2a747e11af | |||
| cda6b4e10d | |||
| 502ce707e1 | |||
| 5b1f73a69e | |||
| f7eba0974f | |||
| 2ae84bccd8 | |||
| 3ac3f1aee5 | |||
| 23ca74afdb | |||
| 7d894fb1e3 | |||
| b39e6be952 | |||
| 415ebf6821 | |||
| b1ec4f7387 | |||
| 820af9b133 | |||
| 54c88dcb2f | |||
| 104ed85d72 | |||
| 84de5927cd | |||
| 1b829e095b | |||
| cbb4624e25 | |||
| 2cb02ff3f2 | |||
| b8cd441109 | |||
| 86a8c23999 | |||
| a2573b5aaf | |||
| ea8671f64b | |||
| a234903b8f | |||
| 981da95cde | |||
| 18fe17f00f | |||
| 5847215097 | |||
| d92b24a2f6 | |||
| 2ced591ed4 | |||
| 56dcd0feb3 | |||
| 0bc1254ce0 | |||
| 190737b915 | |||
| 038191d38e | |||
| 84da78e2e8 | |||
| 13b89eb2a9 | |||
| 89c524a690 | |||
| d44d1b6e64 | |||
| 8ea5b3d5be | |||
| f8c774700e | |||
| 5987fda0b2 | |||
| 0e0eb57fd9 | |||
| 93b8049b01 | |||
| 5cf74b4405 | |||
| 2aa91df820 | |||
| 85fedbfeaf | |||
| 1cbe74252c | |||
| 9fb9667dba | |||
| 55b6ca7392 | |||
| 758c2bf0bc | |||
| 6eed271814 | |||
| 7a5a4bddf8 | |||
| 3f8ea683ef | |||
| 29dff35bb4 | |||
| 79462c320d |
2
.gitignore
vendored
2
.gitignore
vendored
@ -13,3 +13,5 @@ build/
|
|||||||
src/res/img/git-ignore/
|
src/res/img/git-ignore/
|
||||||
|
|
||||||
test/debug-configs/
|
test/debug-configs/
|
||||||
|
|
||||||
|
debugging-resources/
|
||||||
|
|||||||
@ -1,23 +0,0 @@
|
|||||||
image: node:current
|
|
||||||
|
|
||||||
cache:
|
|
||||||
paths:
|
|
||||||
- node_modules/
|
|
||||||
- .yarn
|
|
||||||
|
|
||||||
stages:
|
|
||||||
- prep
|
|
||||||
- build
|
|
||||||
- pack
|
|
||||||
|
|
||||||
install deps:
|
|
||||||
stage: prep
|
|
||||||
script: yarn install
|
|
||||||
|
|
||||||
build:
|
|
||||||
stage: build
|
|
||||||
script: npm run build
|
|
||||||
|
|
||||||
create zip:
|
|
||||||
stage: pack
|
|
||||||
script: npm run build-zip
|
|
||||||
18
.vscode/launch.json
vendored
18
.vscode/launch.json
vendored
@ -4,10 +4,19 @@
|
|||||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"configurations": [
|
"configurations": [
|
||||||
|
|
||||||
|
{
|
||||||
|
"type": "chrome",
|
||||||
|
"request": "attach",
|
||||||
|
"name": "Attach to Chrome",
|
||||||
|
"port": 9222,
|
||||||
|
"urlFilter": "http://*/*",
|
||||||
|
"webRoot": "${workspaceFolder}"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "firefox",
|
"type": "firefox",
|
||||||
"request": "attach",
|
"request": "attach",
|
||||||
"name": "Attach",
|
"name": "Attach (firefox)",
|
||||||
"pathMappings": [
|
"pathMappings": [
|
||||||
{
|
{
|
||||||
"url": "webpack:///ext",
|
"url": "webpack:///ext",
|
||||||
@ -16,7 +25,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Launch addon",
|
"name": "Launch addon (firefox)",
|
||||||
"type": "firefox",
|
"type": "firefox",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"port": 6000,
|
"port": 6000,
|
||||||
@ -36,5 +45,8 @@
|
|||||||
"firefoxArgs": [
|
"firefoxArgs": [
|
||||||
"--start-debugger-server"
|
"--start-debugger-server"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"chrome": {
|
||||||
|
"executable": "/usr/bin/google-chrome-stable --remote-debugging-port=9222",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
43
.vscode/settings.json
vendored
43
.vscode/settings.json
vendored
@ -1,8 +1,7 @@
|
|||||||
{
|
{
|
||||||
"cSpell.words": [
|
"cSpell.words": [
|
||||||
"PILLARBOX",
|
|
||||||
"PILLARBOXED",
|
|
||||||
"aard",
|
"aard",
|
||||||
|
"allowtransparency",
|
||||||
"ardetector",
|
"ardetector",
|
||||||
"autodetect",
|
"autodetect",
|
||||||
"autodetection",
|
"autodetection",
|
||||||
@ -10,46 +9,77 @@
|
|||||||
"blackbar",
|
"blackbar",
|
||||||
"blackbars",
|
"blackbars",
|
||||||
"blackframe",
|
"blackframe",
|
||||||
|
"bruteforce",
|
||||||
"canvas",
|
"canvas",
|
||||||
|
"clickthrough",
|
||||||
|
"cmpa",
|
||||||
|
"cmpb",
|
||||||
"com",
|
"com",
|
||||||
"comms",
|
"comms",
|
||||||
"csui",
|
"csui",
|
||||||
|
"ctab",
|
||||||
|
"cycleable",
|
||||||
"decycle",
|
"decycle",
|
||||||
|
"dinked",
|
||||||
|
"dinks",
|
||||||
"disneyplus",
|
"disneyplus",
|
||||||
|
"endregion",
|
||||||
"equalish",
|
"equalish",
|
||||||
"fith",
|
"fith",
|
||||||
"fitw",
|
"fitw",
|
||||||
"fuckup",
|
"fuckup",
|
||||||
|
"fucky",
|
||||||
|
"geoblocked",
|
||||||
|
"geoblocking",
|
||||||
"gfycat",
|
"gfycat",
|
||||||
"gmail",
|
"gmail",
|
||||||
"guardline",
|
"guardline",
|
||||||
"han",
|
"han",
|
||||||
|
"haram",
|
||||||
|
"Heebo",
|
||||||
|
"hoverable",
|
||||||
"iframe",
|
"iframe",
|
||||||
"imgur",
|
"imgur",
|
||||||
"insta",
|
"insta",
|
||||||
|
"jsoneditor",
|
||||||
|
"jwplayer",
|
||||||
|
"kavin",
|
||||||
"letterboxed",
|
"letterboxed",
|
||||||
"manjaro",
|
"manjaro",
|
||||||
|
"mdicon",
|
||||||
|
"mdijs",
|
||||||
|
"metaivi",
|
||||||
"minification",
|
"minification",
|
||||||
"mitigations",
|
"mitigations",
|
||||||
|
"mstefan",
|
||||||
"nogrow",
|
"nogrow",
|
||||||
"noshrink",
|
"noshrink",
|
||||||
"outro",
|
"outro",
|
||||||
|
"PILLARBOX",
|
||||||
|
"PILLARBOXED",
|
||||||
"polyfill",
|
"polyfill",
|
||||||
|
"problemo",
|
||||||
"recursing",
|
"recursing",
|
||||||
"reddit",
|
"reddit",
|
||||||
"rescan",
|
"rescan",
|
||||||
"resizer",
|
"resizer",
|
||||||
|
"resizers",
|
||||||
|
"SCPI",
|
||||||
"scrollbar",
|
"scrollbar",
|
||||||
"shitiness",
|
"shitiness",
|
||||||
"smallcaps",
|
"smallcaps",
|
||||||
|
"spaghettified",
|
||||||
"suboption",
|
"suboption",
|
||||||
|
"subscan",
|
||||||
"tabitem",
|
"tabitem",
|
||||||
"tablist",
|
"tablist",
|
||||||
"tamius",
|
"tamius",
|
||||||
"textbox",
|
"textbox",
|
||||||
|
"ultrawide",
|
||||||
"ultrawidify",
|
"ultrawidify",
|
||||||
"unmark",
|
"unmark",
|
||||||
"unmarking",
|
"unmarking",
|
||||||
|
"unsets",
|
||||||
"unshift",
|
"unshift",
|
||||||
"uwid",
|
"uwid",
|
||||||
"uwui",
|
"uwui",
|
||||||
@ -57,9 +87,14 @@
|
|||||||
"vids",
|
"vids",
|
||||||
"vuejs",
|
"vuejs",
|
||||||
"vuex",
|
"vuex",
|
||||||
|
"wakanim",
|
||||||
"webextension",
|
"webextension",
|
||||||
"webextensions",
|
"webextensions",
|
||||||
"youtube"
|
"wuckies",
|
||||||
|
"wucky",
|
||||||
|
"ycenter",
|
||||||
|
"youtube",
|
||||||
|
"YYMM"
|
||||||
],
|
],
|
||||||
"cSpell.ignoreWords": [
|
"cSpell.ignoreWords": [
|
||||||
"abcdefghijklmnopqrstuvwxyz",
|
"abcdefghijklmnopqrstuvwxyz",
|
||||||
@ -75,4 +110,4 @@
|
|||||||
"undisable",
|
"undisable",
|
||||||
"vdid"
|
"vdid"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
135
CHANGELOG.md
135
CHANGELOG.md
@ -1,22 +1,137 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## v6.0 (current major)
|
||||||
|
|
||||||
|
### v6.4.0
|
||||||
|
* In-player UI now appears in full-screen even for websites that use top layer
|
||||||
|
* Embedded sites now inherit settings of the parent frame by default
|
||||||
|
* Setting inheritance/overriding is not thoroughly tested and may be full of edge cases.
|
||||||
|
* Added validation to custom aspect ratio entry menu. Corrected parsing of aspect ratios given in the X:Y format, even though aspect ratios should be ideally given as a single number.
|
||||||
|
* Autodetection can now scan for subtitles.
|
||||||
|
* New experimental autodetection (which is secretly just slightly modified subtitle check)
|
||||||
|
|
||||||
### Plans for the future
|
### v6.3.0
|
||||||
|
* Added zoom segment to in-player UI and popup.
|
||||||
|
* Fixed keyboard zoom
|
||||||
|
* Added additional zoom options. If you wonder how zoom options differ from crop, mess around and find out.
|
||||||
|
* Subdomains now inherit same settings as their parent domain by default
|
||||||
|
* Extension popup detects embedded content
|
||||||
|
* Added `www.youtube-nocookie.com` to "officially supported" list
|
||||||
|
|
||||||
* Native builds for Chromium Edge
|
### v6.2.5
|
||||||
* Settings page looks ugly af right now. Maybe fix it some time later
|
|
||||||
* other bug fixes
|
|
||||||
|
|
||||||
## v7.0 (planned major)
|
* Popup appearance changed — UI advertisement panel was moved to the popup header
|
||||||
* WebGL autodetection
|
* Fixed the bug where popup wouldn't be showing the correct settings
|
||||||
|
* Fixed the bug where site settings would default to 'disabled', even if Extension default setting was not disabled.
|
||||||
|
* Added ability to export and import settings from file (ft. developer mode editor)
|
||||||
|
* [dev goodies] Added debug overlay for aard
|
||||||
|
* Fixed an issue where aspect ratio wouldn't get calculated correctly on youtube videos with native aspect ratios other than 16:9
|
||||||
|
* Fixed an issue that would crash the extension if video element didn't have a player element associated with it
|
||||||
|
* Fixed an issue where extension sometimes wouldn't work if video element was grafted/re-parented to a different element
|
||||||
|
|
||||||
## v6.0 (next major)
|
### v6.2.4
|
||||||
|
* [#264](https://github.com/tamius-han/ultrawidify/issues/264) — fixed issue with white screen that affected some youtube users. Special thanks to [SnowyOwlNugget](https://github.com/SnowyOwlNugget">SnowyOwlNugget), who instead of whining provided the necessary information.
|
||||||
|
* Minor updates to the settings
|
||||||
|
* Switching between full screen, theater, and normal player now correctly enables and disables the extension according to the settings.
|
||||||
|
* Don't reset settings if updating conf patches fails.
|
||||||
|
* Darken in-player UI backgrounds until Chrome fixes its backdrop-filter bug
|
||||||
|
|
||||||
* in-player GUI
|
### v6.2.3
|
||||||
* Fix UI logger
|
* Fixed default persistent mode
|
||||||
|
|
||||||
## v5.x (current major)
|
### v6.2.2
|
||||||
|
* Fixed the issue where stretching was not applied if no cropping was used
|
||||||
|
* Added way to manually enable or disable color scheme detection, as a compromise between #259 and #264
|
||||||
|
|
||||||
|
### v6.2.1
|
||||||
|
|
||||||
|
* Fixed offset issue on Netflix
|
||||||
|
* Fixed issue with overlay iframe not being transparent on twitter
|
||||||
|
|
||||||
|
### v6.2.0
|
||||||
|
|
||||||
|
* Parts of automatic aspect ratio detection use WebGL now. This should result in improved performance.
|
||||||
|
* In-player UI now indicates whether a site is having a problem with automatic aspect ratio detection.
|
||||||
|
* UI improvements
|
||||||
|
|
||||||
|
Since automatic aspect ratio detection had to be rewritten completely and since the new version needs to be out 5 days ago, there are some regressions in the automatic aspect ratio detection.
|
||||||
|
|
||||||
|
### v6.1.0
|
||||||
|
|
||||||
|
Was skipped due to how major the changes were.
|
||||||
|
|
||||||
|
### v6.0.1
|
||||||
|
|
||||||
|
* Fixed external links
|
||||||
|
|
||||||
|
### v6.0.0
|
||||||
|
|
||||||
|
Chrome only, because I needed to rush manifest v3 migration before ensuring things work in Firefox.
|
||||||
|
|
||||||
|
* In player UI
|
||||||
|
* New alignment options (can align video both vertically, as well as horizontally)
|
||||||
|
* Extension does a little bit of a better job differentiating between levels of support for a given site
|
||||||
|
* Changed how cropping and panning works. This should lead to fewer problems and better generic support.
|
||||||
|
* REGRESSION: no manual panning with shift + mouse movement
|
||||||
|
* "Player select" screen, which provides a GUI way for picking HTML element that acts as the video player area
|
||||||
|
|
||||||
|
Regressions and potential issues:
|
||||||
|
* Settings were largely not migrated from previous versions. This is as intended, since changes to cropping (and some other aspects)
|
||||||
|
rendered certain old settings obsolete.
|
||||||
|
* Extension can no longer discriminate between iframes — extension popup commands get sent to ALL iframes on page
|
||||||
|
* Extension probably can't discriminate between multiple videos on a single page
|
||||||
|
|
||||||
|
## v5.x
|
||||||
|
|
||||||
|
### v5.1.7
|
||||||
|
|
||||||
|
Firefox-only.
|
||||||
|
|
||||||
|
* When cropping and panning video, CSS' `transform: translate(x,y)` now uses integers _always_. Before that fix, `translate(x,y)` could be offset by x.5 px, and that half of a pixel introduced a 1px thick line on the portion of the left edge of the video.
|
||||||
|
|
||||||
|
### v5.1.7
|
||||||
|
|
||||||
|
* When aligning videos, ensure video is never translated by a fractional value
|
||||||
|
* As of recent nVidia driver update in Edge, Angle detection fails in a way that prevents popup from showing up. This problem should be fixed now.
|
||||||
|
|
||||||
|
The angle detection problem was fixed by disabling the angle detection check for now, as Chrome, et al. appear to have fixed buggy video hardware acceleration.
|
||||||
|
|
||||||
|
### v5.1.6
|
||||||
|
|
||||||
|
* Added new config for disney+ (courtesy of @MStefan99)
|
||||||
|
* Chrome hardware acceleration is bugged and cannot be worked around in-extension. Only way to fix this is for users to change their Google Chrome settings. Added naggathon with instructions into the extension popup.
|
||||||
|
|
||||||
|
### v5.1.5
|
||||||
|
|
||||||
|
* Fixed laginess in Chromium-based browsers on Windows. Details in [#199](https://github.com/tamius-han/ultrawidify/issues/199#issuecomment-1221383134)
|
||||||
|
|
||||||
|
### v5.1.4
|
||||||
|
|
||||||
|
* Fixed some problems with autodetection not returning to 16:9 when necessary if autodetection already changed aspect ratio ([#198](https://github.com/tamius-han/ultrawidify/issues/198))
|
||||||
|
|
||||||
|
### v5.1.3
|
||||||
|
|
||||||
|
* Fixed some problems with autodetection sometimes briefly resetting on dark frames ([#195](https://github.com/tamius-han/ultrawidify/issues/195), [#196](https://github.com/tamius-han/ultrawidify/issues/196))
|
||||||
|
|
||||||
|
### v5.1.2
|
||||||
|
|
||||||
|
* `set-extension-mode` turned into `set-ExtensionMode` at some point. This caused in "Enable this extension" options to vanish on certain setups.
|
||||||
|
* Blackframe tests now run on same data as main algorithm as opposed on a smaller sample (`drawImage()` calls are _very_ expensive even for a 16x9 sample).
|
||||||
|
|
||||||
|
### v5.1.1
|
||||||
|
|
||||||
|
* Fixed autodetection
|
||||||
|
|
||||||
|
### v5.1.0
|
||||||
|
|
||||||
|
* Re-enable logger
|
||||||
|
* Move aspect ratio autodetection to requestAnimationFrame
|
||||||
|
* Fix netflix
|
||||||
|
|
||||||
|
### v5.0.7
|
||||||
|
|
||||||
|
* Videos of square-ish aspect ratios on 1440p (and lower) resolutions now no longer get misaligned ([#162](https://github.com/tamius-han/ultrawidify/issues/162))
|
||||||
|
* Alignment of featured videos on youtube channel page should now also be fixed
|
||||||
|
|
||||||
### v5.0.6
|
### v5.0.6
|
||||||
* Added configuration for metaivi.com based on user feedback ([#160](https://github.com/tamius-han/ultrawidify/issues/160))
|
* Added configuration for metaivi.com based on user feedback ([#160](https://github.com/tamius-han/ultrawidify/issues/160))
|
||||||
|
|||||||
39
CONTRIBUTING.md
Normal file
39
CONTRIBUTING.md
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
# Contributing to Ultrawidify
|
||||||
|
|
||||||
|
Thank you for considering contributing to this project! We welcome contributions from the community and are grateful for your efforts.
|
||||||
|
|
||||||
|
## How to Contribute
|
||||||
|
|
||||||
|
0. Open an issue, where you introduce the feature you want to add or thing you want to fix
|
||||||
|
1. Fork the repository.
|
||||||
|
2. Create a new branch for your contribution.
|
||||||
|
3. Write readable code
|
||||||
|
4. Check that your additions do not break existing features
|
||||||
|
5. Open a pull request
|
||||||
|
|
||||||
|
## Code of Conduct
|
||||||
|
|
||||||
|
Please follow the code of conduct:
|
||||||
|
|
||||||
|
* don't be too much of an asshole
|
||||||
|
* language policing my code comments is not a worthwhile contribution, and is considered spam. If you're here to do that, fuck off. I'm looking at you, ScamOSS and co.
|
||||||
|
|
||||||
|
## Code Style
|
||||||
|
|
||||||
|
- Use formatting consistent with the rest of the project
|
||||||
|
- Write meaningful commit messages.
|
||||||
|
- Include comments where necessary to explain complex logic.
|
||||||
|
|
||||||
|
## Licensing & Contribution Agreement
|
||||||
|
|
||||||
|
By submitting a contribution (e.g., via pull request), you agree to the following:
|
||||||
|
|
||||||
|
- You grant the project maintainer(s) a non-exclusive, irrevocable, worldwide, royalty-free license to use, copy, modify, and distribute your contributions as part of the project.
|
||||||
|
- You certify that your contributions are your original work and that you have the right to submit them.
|
||||||
|
- You agree that your contributions are licensed under the same license as the rest of the project, unless explicitly stated otherwise.
|
||||||
|
|
||||||
|
**TL;DR:** once your code is in the project, there's no take-backsies.
|
||||||
|
|
||||||
|
## Questions?
|
||||||
|
|
||||||
|
The discussion board is right over there, three tabs to the right.
|
||||||
6
DOCUMENTATION.MD
Normal file
6
DOCUMENTATION.MD
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
# Implementation details
|
||||||
|
|
||||||
|
## Enabling/disabling aspect ratio corrections
|
||||||
|
|
||||||
|
* Aspect ratios are changed by proxy. Extension attaches **a custom CSS class** to `video` and `player` elements.
|
||||||
|
* To prevent extension from affecting the appearance of a webpage, **it's sufficient to remove our custom CSS classes from `video` and `player` elements.**
|
||||||
32
Jenkinsfile
vendored
Normal file
32
Jenkinsfile
vendored
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
// required jenkins plugins:
|
||||||
|
// * https://plugins.jenkins.io/git/
|
||||||
|
|
||||||
|
pipeline {
|
||||||
|
agent any
|
||||||
|
|
||||||
|
stages {
|
||||||
|
|
||||||
|
// stage('Check for changes') {
|
||||||
|
// sh "env.GIT_COMMIT != env.GIT_PREVIOUS_COMMIT"
|
||||||
|
// }
|
||||||
|
|
||||||
|
stage('Install dependencies') {
|
||||||
|
steps {
|
||||||
|
sh 'npm ci'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Build') {
|
||||||
|
steps {
|
||||||
|
sh 'npm run build-all'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Push to release server') {
|
||||||
|
steps {
|
||||||
|
sh "echo 'implement me pls!'"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
35
LICENSE.MD
Normal file
35
LICENSE.MD
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
# License
|
||||||
|
|
||||||
|
Copyright (c) 2025 @tamius-han
|
||||||
|
|
||||||
|
The source code is provided on the "you can look, but you can't take" basis.
|
||||||
|
|
||||||
|
## Permissions
|
||||||
|
|
||||||
|
You are permitted to:
|
||||||
|
|
||||||
|
* You can view the code
|
||||||
|
* You can create forks and modify the code for personal use
|
||||||
|
* You can build and run modified versions of this project for personal use on your local machine
|
||||||
|
* Push modifications to your personal github fork
|
||||||
|
|
||||||
|
## Restrictions
|
||||||
|
|
||||||
|
You may NOT:
|
||||||
|
|
||||||
|
* Distribute this project or any modified versions outside of your personal Github fork
|
||||||
|
* Publish or distribute compiled binaries, builds, or packages of this project or any derivative work
|
||||||
|
* Use the code in commercial products, services, or other public-facing deployments.
|
||||||
|
* Sub-license, sell, or otherwise make the software or derivatives available to third parties.
|
||||||
|
|
||||||
|
## Contributions
|
||||||
|
|
||||||
|
By submitting a pull request or other contribution, you agree that:
|
||||||
|
|
||||||
|
- You grant the project maintainer(s) a non-exclusive, irrevocable, worldwide, royalty-free license to use, copy, modify, and distribute your contributions as part of the project.
|
||||||
|
- You certify that your contributions are your original work and that you have the right to submit them.
|
||||||
|
- You agree that your contributions are licensed under the same license as the rest of the project, unless explicitly stated otherwise.
|
||||||
|
|
||||||
|
## Disclaimer
|
||||||
|
|
||||||
|
This software is provided "as is", without warranty of any kind, express or implied. Use at your own risk.
|
||||||
55570
package-lock.json
generated
55570
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
130
package.json
130
package.json
@ -1,76 +1,88 @@
|
|||||||
{
|
{
|
||||||
"name": "ultrawidify",
|
"name": "ultrawidify",
|
||||||
"version": "5.0.6",
|
"version": "6.3.998",
|
||||||
"description": "Aspect ratio fixer for youtube and other sites, with automatic aspect ratio detection. Supports ultrawide and other ratios.",
|
"description": "Aspect ratio fixer for youtube and other sites, with automatic aspect ratio detection. Supports ultrawide and other ratios.",
|
||||||
"author": "Tamius Han <tamius.han@gmail.com>",
|
"author": "Tamius Han <tamius.han@gmail.com>",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "npm run pre-build; cross-env NODE_ENV=production BROWSER=firefox CHANNEL=stable webpack --hide-modules",
|
"build": "npm run pre-build; cross-env NODE_ENV=production BROWSER=firefox CHANNEL=stable webpack",
|
||||||
"build-all": "bash ./scripts/build-all.sh",
|
"build-all": "bash ./scripts/build-all.sh",
|
||||||
"build-chrome": "cross-env NODE_ENV=production BROWSER=chrome CHANNEL=stable webpack --hide-modules",
|
"build-chrome": "cross-env NODE_ENV=production BROWSER=chrome CHANNEL=stable webpack",
|
||||||
"build-chrome:dev": "cross-env NODE_ENV=development BROWSER=chrome webpack --hide-modules",
|
"build-chrome:dev": "cross-env NODE_ENV=development BROWSER=chrome webpack",
|
||||||
"build-edge": "cross-env NODE_ENV=production BROWSER=edge CHANNEL=stable webpack --hide-modules",
|
"build-edge": "cross-env NODE_ENV=production BROWSER=edge CHANNEL=stable webpack",
|
||||||
"build-nightly": "cross-env NODE_ENV=development BROWSER=firefox CHANNEL=nightly webpack --hide-modules",
|
"build-nightly": "cross-env NODE_ENV=development BROWSER=firefox CHANNEL=nightly webpack",
|
||||||
"build-nightly-chrome": "cross-env NODE_ENV=development BROWSER=chrome CHANNEL=nightly webpack --hide-modules",
|
"build-nightly-chrome": "cross-env NODE_ENV=development BROWSER=chrome CHANNEL=nightly webpack",
|
||||||
"build-testing": "cross-env NODE_ENV=development BROWSER=firefox CHANNEL=testing webpack --hide-modules",
|
"build-testing": "cross-env NODE_ENV=development BROWSER=firefox CHANNEL=testing webpack",
|
||||||
"build-testing-chrome": "cross-env NODE_ENV=development BROWSER=chrome CHANNEL=testing webpack --hide-modules",
|
"build-testing-chrome": "cross-env NODE_ENV=development BROWSER=chrome CHANNEL=testing webpack",
|
||||||
"build-zip": "node scripts/build-zip.js",
|
"build-zip": "node scripts/build-zip.js",
|
||||||
"build:dev": "webpack --hide-modules",
|
"build:dev": "webpack",
|
||||||
"dev": "cross-env NODE_ENV=development CHANNEL=dev concurrently \"cross-env BROWSER=firefox npm run build:dev -- --watch\" \"cross-env BROWSER=chrome npm run build:dev -- --watch\" \"cross-env BROWSER=edge npm run build:dev -- --watch\"",
|
"dev": "cross-env NODE_ENV=development CHANNEL=dev concurrently \"cross-env BROWSER=firefox npm run build:dev -- --watch\" \"cross-env BROWSER=chrome npm run build:dev -- --watch\" \"cross-env BROWSER=edge npm run build:dev -- --watch\"",
|
||||||
"dev-ff": "cross-env NODE_ENV=development CHANNEL=dev BROWSER=firefox npm run build:dev -- --watch",
|
"dev:windows": "cross-env NODE_ENV=development CHANNEL=dev concurrently \"cross-env BROWSER=firefox npm run build:dev -- -w --watch-poll\" \"cross-env BROWSER=chrome npm run build:dev -- -w --watch-poll\" \"cross-env BROWSER=edge npm run build:dev -- -w --watch-poll\"",
|
||||||
"dev-chrome": "cross-env NODE_ENV=development CHANNEL=dev BROWSER=chrome npm run build:dev -- --watch",
|
"dev-ff": "rm -rf ./dist-ff && cross-env NODE_ENV=development CHANNEL=dev BROWSER=firefox npm run build:dev -- --watch",
|
||||||
"dev-edge": "cross-env NODE_ENV=development CHANNEL=dev BROWSER=edge npm run build:dev -- --watch",
|
"dev-chrome": "rm -rf ./dist-chrome && cross-env NODE_ENV=development CHANNEL=dev BROWSER=chrome npm run build:dev -- --watch",
|
||||||
"pre-build": "rm -rf ./dist-ff; rm -rf ./dist-chrome; rm -rf ./node_modules; npm ci",
|
"dev-edge": "rm -rf ./dist-edge && cross-env NODE_ENV=development CHANNEL=dev BROWSER=edge npm run build:dev -- --watch",
|
||||||
"start": "npm run dev"
|
"pre-build": "rm -rf ./dist-ff; rm -rf ./dist-chrome; rm -rf ./dist-edge; rm -rf ./node_modules; npm ci",
|
||||||
|
"start": "npm run dev",
|
||||||
|
"start:windows": "npm run dev:windows"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/plugin-proposal-class-properties": "^7.12.1",
|
"@babel/plugin-proposal-class-properties": "^7.18.6",
|
||||||
"@types/chrome": "0.0.129",
|
"@mdi/font": "^7.4.47",
|
||||||
"@types/core-js": "^2.5.3",
|
"@mdi/js": "^7.4.47",
|
||||||
"@types/es6-promise": "^3.3.0",
|
"@types/resize-observer-browser": "^0.1.11",
|
||||||
"@types/firefox": "0.0.30",
|
"concurrently": "^9.2.1",
|
||||||
"@types/resize-observer-browser": "^0.1.5",
|
"fs-extra": "^11.3.2",
|
||||||
"@vue/cli": "^4.5.9",
|
"gl-matrix": "^3.4.4",
|
||||||
"@vue/cli-plugin-typescript": "^4.5.11",
|
"json-cyclic": "1.0.2",
|
||||||
"bootstrap": "^4.5.3",
|
"json-difference": "^1.16.1",
|
||||||
"bootstrap-icons": "^1.1.0",
|
|
||||||
"bootstrap-icons-vue": "^0.3.0",
|
|
||||||
"bootstrap-vue": "^2.20.1",
|
|
||||||
"concurrently": "^5.2.0",
|
|
||||||
"fs-extra": "^7.0.1",
|
|
||||||
"json-cyclic": "0.0.3",
|
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"typescript": "^4.2.3",
|
"mdi-vue": "^3.0.13",
|
||||||
"vue": "^3.0.0-beta.1",
|
"typescript": "^5.9.3",
|
||||||
"vuex": "^4.0.0-alpha.1",
|
"vanilla-jsoneditor": "^3.10.0",
|
||||||
"vuex-webextensions": "^1.3.0",
|
"vue": "^3.5.25",
|
||||||
"webextension-polyfill-ts": "^0.24.0"
|
"vue-style-loader": "^4.1.3",
|
||||||
|
"vuex": "^4.1.0",
|
||||||
|
"vuex-webextensions": "^1.3.3",
|
||||||
|
"webextension-polyfill": "^0.12.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.12.13",
|
"@babel/core": "^7.28.5",
|
||||||
"@babel/plugin-proposal-optional-chaining": "^7.10.4",
|
"@babel/preset-env": "^7.28.5",
|
||||||
"@babel/preset-env": "^7.12.13",
|
"@tailwindcss/postcss": "^4.1.17",
|
||||||
"@types/lodash": "^4.14.168",
|
"@types/chrome": "0.1.31",
|
||||||
"@types/node": "^14.14.25",
|
"@types/core-js": "^2.5.8",
|
||||||
"@vue/compiler-sfc": "^3.0.3",
|
"@types/es6-promise": "^3.3.2",
|
||||||
"archiver": "^3.0.0",
|
"@types/firefox": "0.0.34",
|
||||||
"babel-loader": "^8.2.2",
|
"@types/lodash": "^4.17.21",
|
||||||
|
"@types/node": "^24.10.1",
|
||||||
|
"@vue/cli": "^5.0.9",
|
||||||
|
"@vue/cli-plugin-typescript": "^5.0.9",
|
||||||
|
"@vue/compiler-sfc": "^3.5.25",
|
||||||
|
"archiver": "^7.0.1",
|
||||||
|
"babel-loader": "^10.0.0",
|
||||||
"babel-preset-es2020": "^1.0.2",
|
"babel-preset-es2020": "^1.0.2",
|
||||||
"copy-webpack-plugin": "^4.5.3",
|
"copy-webpack-plugin": "^13.0.1",
|
||||||
"cross-env": "^5.2.0",
|
"cross-env": "^10.1.0",
|
||||||
"css-loader": "^0.28.11",
|
"css-loader": "^7.1.2",
|
||||||
"ejs": "^2.7.4",
|
"ejs": "^3.1.10",
|
||||||
"file-loader": "^1.1.11",
|
"file-loader": "^6.2.0",
|
||||||
"mini-css-extract-plugin": "^0.4.4",
|
"mini-css-extract-plugin": "^2.9.4",
|
||||||
"node-sass": "^4.14.1",
|
"postcss": "^8.5.6",
|
||||||
"sass-loader": "^7.1.0",
|
"postcss-import": "^16.1.1",
|
||||||
"ts-loader": "^8.0.16",
|
"postcss-nested": "^7.0.2",
|
||||||
|
"postcss-preset-env": "^10.4.0",
|
||||||
|
"postcss-strip-inline-comments": "^0.1.5",
|
||||||
|
"raw-loader": "^4.0.2",
|
||||||
|
"resolve-url-loader": "^5.0.0",
|
||||||
|
"tailwindcss": "^4.1.17",
|
||||||
|
"ts-loader": "^9.5.4",
|
||||||
|
"tsconfig-paths": "^4.2.0",
|
||||||
"vue-cli-plugin-vue-next": "~0.1.4",
|
"vue-cli-plugin-vue-next": "~0.1.4",
|
||||||
"vue-loader": "^16.0.0",
|
"vue-loader": "^17.4.2",
|
||||||
"web-ext-types": "^2.3.0",
|
"vue-tsc": "^3.1.5",
|
||||||
"webextension-polyfill": "^0.6.0",
|
"web-ext-types": "^3.2.1",
|
||||||
"webpack": "^4.44.0",
|
"webpack": "^5.103.0",
|
||||||
"webpack-chrome-extension-reloader": "^0.8.3",
|
"webpack-cli": "^6.0.1",
|
||||||
"webpack-cli": "^3.3.12",
|
"webpack-shell-plugin": "^0.5.0",
|
||||||
"webpack-shell-plugin": "^0.5.0"
|
"webpack-shell-plugin-next": "^2.3.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
10
paths.config.js
Normal file
10
paths.config.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
'@src': path.resolve(__dirname, 'src'),
|
||||||
|
'@ui': path.resolve(__dirname, 'src/ui'),
|
||||||
|
'@components': path.resolve(__dirname, 'src/ui/components'),
|
||||||
|
'@res': path.resolve(__dirname, 'src/ui/res'),
|
||||||
|
'@': path.resolve(__dirname, 'src'),
|
||||||
|
};
|
||||||
|
|
||||||
24
postcss.config.js
Normal file
24
postcss.config.js
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
const path = require('path');
|
||||||
|
const aliases = require('./paths.config');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
plugins: [
|
||||||
|
require('postcss-import')({
|
||||||
|
resolve(id) {
|
||||||
|
for (const alias in aliases) {
|
||||||
|
if (id === alias || id.startsWith(alias + '/')) {
|
||||||
|
return path.resolve(
|
||||||
|
aliases[alias],
|
||||||
|
id.slice(alias.length + 1)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return id
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
require('@tailwindcss/postcss'),
|
||||||
|
require('postcss-nested'),
|
||||||
|
require('autoprefixer'),
|
||||||
|
],
|
||||||
|
};
|
||||||
@ -4,17 +4,33 @@
|
|||||||
# command from the root directory of the project. Running it in
|
# command from the root directory of the project. Running it in
|
||||||
# any other way probably isn't going to work.
|
# any other way probably isn't going to work.
|
||||||
|
|
||||||
# pre-build steps:
|
# pre-build steps:
|
||||||
|
|
||||||
|
echo "—————————————————————————————————————————————————————";
|
||||||
|
echo " BUILDING ALL ULTRAWIDIFY VERSIONS";
|
||||||
|
echo "—————————————————————————————————————————————————————";
|
||||||
|
|
||||||
mkdir -p ./build/old
|
mkdir -p ./build/old
|
||||||
npm run pre-build
|
npm run pre-build
|
||||||
|
|
||||||
|
echo "::"
|
||||||
|
echo "::"
|
||||||
|
echo "::"
|
||||||
|
echo ":: Pre-build complete. Moving old builds from dist-zip ..."
|
||||||
|
|
||||||
rm ./dist-zip/uw-amo-source.zip
|
rm ./dist-zip/uw-amo-source.zip
|
||||||
mv -f ./dist-zip/*.zip ./build/old
|
mv -f ./dist-zip/*.zip ./build/old
|
||||||
|
|
||||||
|
echo "::"
|
||||||
|
echo "::"
|
||||||
|
echo "::"
|
||||||
|
echo ":: dist-zip is clean. Starting builds ..."
|
||||||
|
|
||||||
# lets force raise ram limit, but the improper way
|
# lets force raise ram limit, but the improper way
|
||||||
# export NODE_OPTIONS=--max_old_space_size=4096
|
# export NODE_OPTIONS=--max_old_space_size=4096
|
||||||
|
|
||||||
# build the version for each browser and create a zip afterwards
|
# build the version for each browser and create a zip afterwards
|
||||||
# step 1: define build functions
|
# step 1: define build functions
|
||||||
#function buildFF {
|
#function buildFF {
|
||||||
npm run build
|
npm run build
|
||||||
node scripts/build-zip.js ff
|
node scripts/build-zip.js ff
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
# Script assumes we're in basedir of the repository and that extension has been built and zipped to /dist-zip
|
# Script assumes we're in basedir of the repository and that extension has been built and zipped to /dist-zip
|
||||||
# using the crx name we want (same name as zip, except different extension)
|
# using the crx name we want (same name as zip, except different extension)
|
||||||
#
|
#
|
||||||
# also this doesn't check for errors ever so
|
# also this doesn't check for errors ever so
|
||||||
#
|
#
|
||||||
|
|
||||||
@ -42,4 +42,4 @@ sig_len_hex=$(byte_swap $(printf '%08x\n' $(ls -l "$sig" | awk '{print $5}')))
|
|||||||
echo "Wrote $crx"
|
echo "Wrote $crx"
|
||||||
|
|
||||||
echo "exiting dist-zip"
|
echo "exiting dist-zip"
|
||||||
cd ..
|
cd ..
|
||||||
|
|||||||
@ -24,7 +24,7 @@ const buildZip = (src, dist, zipFilename) => {
|
|||||||
|
|
||||||
const archive = archiver('zip', { zlib: { level: 9 }});
|
const archive = archiver('zip', { zlib: { level: 9 }});
|
||||||
const stream = fs.createWriteStream(path.join(dist, zipFilename));
|
const stream = fs.createWriteStream(path.join(dist, zipFilename));
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
archive
|
archive
|
||||||
.directory(src, false)
|
.directory(src, false)
|
||||||
@ -46,14 +46,14 @@ const main = () => {
|
|||||||
browserPostfix = browser;
|
browserPostfix = browser;
|
||||||
}
|
}
|
||||||
const destDir = path.join(__dirname, `../dist-${browserPostfix}`);
|
const destDir = path.join(__dirname, `../dist-${browserPostfix}`);
|
||||||
const zipDir = path.join(__dirname, '../dist-zip');
|
const zipDir = path.join(__dirname, '../dist-zip');
|
||||||
const {name, version} = extractExtensionData(browserPostfix);
|
const {name, version} = extractExtensionData(browserPostfix);
|
||||||
|
|
||||||
// collapse spaces and dashes into single dash
|
// collapse spaces and dashes into single dash
|
||||||
const baseFilename = `${name.replace(/[ -]+/g, '-')}-${version}`;
|
const baseFilename = `${name.replace(/[ -]+/g, '-')}-${version}`;
|
||||||
|
|
||||||
let realZipDir;
|
let realZipDir;
|
||||||
|
|
||||||
if (!!testingOrNightly) {
|
if (!!testingOrNightly) {
|
||||||
realZipDir = path.join(zipDir, version);
|
realZipDir = path.join(zipDir, version);
|
||||||
} else {
|
} else {
|
||||||
@ -61,7 +61,7 @@ const main = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const zipFilename = `${baseFilename}-${browser}.zip`;
|
const zipFilename = `${baseFilename}-${browser}.zip`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
makeDirIfNotExists(realZipDir, {recursive: true});
|
makeDirIfNotExists(realZipDir, {recursive: true});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -70,7 +70,7 @@ const main = () => {
|
|||||||
}
|
}
|
||||||
buildZip(destDir, realZipDir, zipFilename)
|
buildZip(destDir, realZipDir, zipFilename)
|
||||||
.then(() => console.info('OK'))
|
.then(() => console.info('OK'))
|
||||||
.catch(console.err);
|
.catch(console.err);
|
||||||
};
|
};
|
||||||
|
|
||||||
main();
|
main();
|
||||||
|
|||||||
@ -5,8 +5,7 @@ const fs = require('fs');
|
|||||||
|
|
||||||
const BUNDLE_DIR = path.join(__dirname, `../dist-${process.env.BROWSER === 'firefox' ? 'ff' : process.env.BROWSER}`);
|
const BUNDLE_DIR = path.join(__dirname, `../dist-${process.env.BROWSER === 'firefox' ? 'ff' : process.env.BROWSER}`);
|
||||||
const bundles = [
|
const bundles = [
|
||||||
'popup/popup.js',
|
// 'csui/csui-popup.js',
|
||||||
'options/options.js',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const evalRegexForProduction = /;([a-z])=function\(\){return this}\(\);try{\1=\1\|\|Function\("return this"\)\(\)\|\|\(0,eval\)\("this"\)}catch\(t\){"object"==typeof window&&\(\1=window\)}/g;
|
const evalRegexForProduction = /;([a-z])=function\(\){return this}\(\);try{\1=\1\|\|Function\("return this"\)\(\)\|\|\(0,eval\)\("this"\)}catch\(t\){"object"==typeof window&&\(\1=window\)}/g;
|
||||||
|
|||||||
@ -1,249 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="action">
|
|
||||||
<div class="flex-row action-name-cmd-container">
|
|
||||||
<div class="">
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<div class="flex action-name">
|
|
||||||
<span v-if="action.cmd && action.cmd.length > 1 || action.cmd[0].action === 'set-ar' && action.userAdded || (action.cmd[0].arg === AspectRatioType.Fixed)" class="icon red" @click="removeAction()">🗙</span>
|
|
||||||
<span v-else class="icon transparent">🗙</span>
|
|
||||||
<span class="icon" @click="editAction()">🖉</span>
|
|
||||||
{{action.name}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="command-details">
|
|
||||||
<div class="flex flex-column cmd-container cd-pad">
|
|
||||||
<div class="flex bold">
|
|
||||||
Command:
|
|
||||||
</div>
|
|
||||||
<div class="flex cmdlist">
|
|
||||||
{{parseCommand(action.cmd)}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- SCOPES -->
|
|
||||||
<div class="flex flex-column scopes-container cd-pad">
|
|
||||||
<div class="flex bold">Popup tabs:</div>
|
|
||||||
|
|
||||||
<!-- global scope -->
|
|
||||||
<div v-if="action.scopes.global"
|
|
||||||
class="flex flex-row scope-row"
|
|
||||||
>
|
|
||||||
<div class="flex scope-scope">
|
|
||||||
Global:
|
|
||||||
</div>
|
|
||||||
<div class="flex scope-row-label scope-visible">
|
|
||||||
Visible? <span class="scope-row-highlight">{{action.scopes.global.show ? 'Yes' : 'No'}}</span>
|
|
||||||
</div>
|
|
||||||
<div v-if="action.scopes.global.show"
|
|
||||||
class="flex scope-row-label scope-button-label"
|
|
||||||
>
|
|
||||||
Button label?
|
|
||||||
<span class="scope-row-highlight">
|
|
||||||
{{action.scopes.global.label ? action.scopes.global.label : (action.label ? action.label : '')}}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex scope-row-label">
|
|
||||||
Keyboard shortcut:
|
|
||||||
<span class="scope-row-highlight">
|
|
||||||
{{action.scopes.global.shortcut ? parseActionShortcut(action.scopes.global.shortcut[0]) : 'None'}}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- site scope -->
|
|
||||||
<div v-if="action.scopes.site"
|
|
||||||
class="flex flex-row scope-row"
|
|
||||||
>
|
|
||||||
<div class="flex scope-scope">
|
|
||||||
Site:
|
|
||||||
</div>
|
|
||||||
<div class="flex scope-row-label scope-visible">
|
|
||||||
Visible? <span class="scope-row-highlight">{{action.scopes.site.show ? 'Yes' : 'No'}}</span>
|
|
||||||
</div>
|
|
||||||
<div v-if="action.scopes.site.show"
|
|
||||||
class="flex scope-row-label scope-button-label"
|
|
||||||
>
|
|
||||||
Button label?
|
|
||||||
<span class="scope-row-highlight">
|
|
||||||
{{action.scopes.site.label ? action.scopes.site.label : (action.label ? action.label : '')}}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex scope-row-label">
|
|
||||||
Keyboard shortcut:
|
|
||||||
<span class="scope-row-highlight">
|
|
||||||
{{action.scopes.site.shortcut ? parseActionShortcut(action.scopes.site.shortcut[0]) : 'None'}}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- page scope -->
|
|
||||||
<div v-if="action.scopes.page"
|
|
||||||
class="flex flex-row scope-row"
|
|
||||||
>
|
|
||||||
<div class="flex scope-scope">
|
|
||||||
Page:
|
|
||||||
</div>
|
|
||||||
<div class="flex scope-row-label scope-visible">
|
|
||||||
Visible? <span class="scope-row-highlight">{{action.scopes.page.show ? 'Yes' : 'No'}}</span>
|
|
||||||
</div>
|
|
||||||
<div v-if="action.scopes.page.show"
|
|
||||||
class="flex scope-row-label scope-button-label"
|
|
||||||
>
|
|
||||||
Button label?
|
|
||||||
<span class="scope-row-highlight">
|
|
||||||
{{action.scopes.page.label ? action.scopes.page.label : (action.label ? action.label : '')}}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex scope-row-label">
|
|
||||||
Keyboard shortcut:
|
|
||||||
<span class="scope-row-highlight">
|
|
||||||
{{action.scopes.page.shortcut ? parseActionShortcut(action.scopes.page.shortcut[0]) : 'None'}}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import StretchType from '../enums/StretchType.enum';
|
|
||||||
import AspectRatioType from '../enums/AspectRatioType.enum';
|
|
||||||
import KeyboardShortcutParser from '../js/KeyboardShortcutParser';
|
|
||||||
|
|
||||||
|
|
||||||
export default {
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
StretchType: StretchType,
|
|
||||||
AspectRatioType: AspectRatioType,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created () {
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
action: Object,
|
|
||||||
index: Number,
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
parseActionShortcut(shortcut) {
|
|
||||||
return KeyboardShortcutParser.parseShortcut(shortcut);
|
|
||||||
},
|
|
||||||
parseCommand(cmd) {
|
|
||||||
let cmdstring = '';
|
|
||||||
for (const c of cmd) {
|
|
||||||
cmdstring += `${c.action} ${c.arg}${c.persistent ? ' persistent' : ''}; `;
|
|
||||||
}
|
|
||||||
return cmdstring;
|
|
||||||
},
|
|
||||||
editAction() {
|
|
||||||
this.$emit('edit');
|
|
||||||
},
|
|
||||||
removeAction() {
|
|
||||||
this.$emit('remove');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import '../../res/css/colors.scss';
|
|
||||||
|
|
||||||
.uw-ultrawidify-container-root {
|
|
||||||
.action {
|
|
||||||
cursor: pointer;
|
|
||||||
position: relative;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action .command-details {
|
|
||||||
height: 0px;
|
|
||||||
max-height: 0px;
|
|
||||||
transition: max-height 0.5s ease;
|
|
||||||
overflow: hidden;
|
|
||||||
transition: height 0.5s ease;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
width: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action:hover .command-details {
|
|
||||||
height: auto;
|
|
||||||
max-height: 200px;
|
|
||||||
transition: max-height 0.5s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.command-details {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
width: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-name-cmd-container, .p1rem {
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cd-pad {
|
|
||||||
padding: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-name {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
font-weight: 300;
|
|
||||||
color: $text-normal;
|
|
||||||
width: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-name:hover, .action:hover .action-name {
|
|
||||||
color: lighten($primary-color, 20%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.red {
|
|
||||||
color: $primary-color !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cmd-container {
|
|
||||||
width: 13.37rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cmdlist {
|
|
||||||
font-family: 'Overpass Mono';
|
|
||||||
font-size: 0.9rem;
|
|
||||||
color: $text-dim;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bold {
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scope-scope {
|
|
||||||
width: 5rem;
|
|
||||||
text-align: right !important;
|
|
||||||
color: $secondary-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scope-visible {
|
|
||||||
width: 7rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scope-button-label {
|
|
||||||
width: 16rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scope-row-label {
|
|
||||||
color: $text-dark;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scope-row-highlight {
|
|
||||||
color: $text-normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.transparent {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,76 +0,0 @@
|
|||||||
<template>
|
|
||||||
<tr>
|
|
||||||
<td class="cmd monospace">{{parseCommand(action.cmd)}}</td>
|
|
||||||
<td class="">{{action.label ? action.label : ""}}</td>
|
|
||||||
<td class="shortcut center-text">{{parseActionShortcut(action)}}</td>
|
|
||||||
<td class="center-text">
|
|
||||||
<div class="checkbox-container">
|
|
||||||
<span class="checkbox"
|
|
||||||
:class="{'checkbox-checked': (action.shortcut && action.shortcut.length &&
|
|
||||||
(action.shortcut[0].onMouseMove || action.shortcut[0].onClick ||
|
|
||||||
action.shortcut[0].onScroll))}"
|
|
||||||
></span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="center-text">
|
|
||||||
<div class="checkbox-container">
|
|
||||||
<span class="checkbox"
|
|
||||||
:class="{'checkbox-checked': action.popup_global}"
|
|
||||||
>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="center-text">
|
|
||||||
<div class="checkbox-container">
|
|
||||||
<span class="checkbox"
|
|
||||||
:class="{'checkbox-checked': action.popup_site}"
|
|
||||||
></span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="center-text">
|
|
||||||
<div class="checkbox-container">
|
|
||||||
<span class="checkbox"
|
|
||||||
:class="{'checkbox-checked': action.popup}"
|
|
||||||
></span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="center-text">
|
|
||||||
<div class="checkbox-container">
|
|
||||||
<span class="checkbox"
|
|
||||||
:class="{'checkbox-checked': action.ui}"
|
|
||||||
></span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>{{action.ui_path ? action.ui_path : ""}}</td>
|
|
||||||
</tr>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import StretchType from '../enums/StretchType.enum';
|
|
||||||
import KeyboardShortcutParser from '../js/KeyboardShortcutParser'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
StretchType: StretchType
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created () {
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
action: Object
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
parseActionShortcut(action) {
|
|
||||||
return KeyboardShortcutParser.parseShortcut(action.shortcut[0]);
|
|
||||||
},
|
|
||||||
parseCommand(cmd) {
|
|
||||||
let cmdstring = '';
|
|
||||||
for(const c of cmd) {
|
|
||||||
cmdstring += `${c.action} ${c.arg}${c.persistent ? ' persistent' : ''}; `;
|
|
||||||
}
|
|
||||||
return cmdstring;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="button center-text flex"
|
|
||||||
:class="{'setting-selected': selected, 'w24': fixedWidth, 'flex-auto': !fixedWidth }"
|
|
||||||
>
|
|
||||||
{{label}}
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
props: {
|
|
||||||
fixedWidth: Boolean,
|
|
||||||
selected: Boolean,
|
|
||||||
label: String,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
File diff suppressed because one or more lines are too long
@ -1,83 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="flex flex-column json-level-indent">
|
|
||||||
<div class="flex flex-row" @click="expanded_internal = !expanded_internal">
|
|
||||||
<div v-if="value_internal.key" class="item-key">
|
|
||||||
"{{value_internal.key}}" <b>:</b>
|
|
||||||
<span v-if="!expanded_internal"><b> [</b> ... <b>]</b>,</span>
|
|
||||||
<template v-else><b>[</b></template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-for="(row, key) of value_internal.value"
|
|
||||||
:key="key"
|
|
||||||
>
|
|
||||||
<JsonArray v-if="Array.isArray(row)"
|
|
||||||
:value="row"
|
|
||||||
:ignoreKeys="ignoreKeys"
|
|
||||||
@change="changeItem(rowKey, $event)"
|
|
||||||
>
|
|
||||||
</JsonArray>
|
|
||||||
<JsonObject v-else-if="typeof row === 'object' && row !== null"
|
|
||||||
:value="row"
|
|
||||||
:label="rowKey"
|
|
||||||
:ignoreKeys="ignoreKeys"
|
|
||||||
@change="changeItem(rowKey, $event)"
|
|
||||||
>
|
|
||||||
</JsonObject>
|
|
||||||
<JsonElement v-else
|
|
||||||
:value="row"
|
|
||||||
:label="rowKey"
|
|
||||||
@change="changeItem(rowKey, $event)"
|
|
||||||
>
|
|
||||||
</JsonElement>
|
|
||||||
</div>
|
|
||||||
<div v-if="expanded_internal"><b>],</b></div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import JsonObject from './JsonObject';
|
|
||||||
import JsonElement from './JsonElement';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'JsonArray',
|
|
||||||
props: [
|
|
||||||
'value',
|
|
||||||
'expanded',
|
|
||||||
'ignoreKeys', // this prop is passthrough for JsonArray
|
|
||||||
],
|
|
||||||
components: {
|
|
||||||
JsonObject,
|
|
||||||
JsonElement,
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
value_internal: undefined,
|
|
||||||
expanded_internal: true,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created() {
|
|
||||||
this.value_internal = this.value;
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
value: function(val) {
|
|
||||||
this.value_internal = val;
|
|
||||||
},
|
|
||||||
expanded: function(val) {
|
|
||||||
if (val !== undefined && val !== null) {
|
|
||||||
this.expanded_internal = !!val;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
changeItem(key, value) {
|
|
||||||
this.value_internal[key] = value;
|
|
||||||
this.$emit('change', this.value_internal);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped src="./json.scss">
|
|
||||||
</style>
|
|
||||||
<style lang="scss" scoped src="../../../res/css/flex.scss">
|
|
||||||
</style>
|
|
||||||
@ -1,90 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="flex flex-row json-level-indent">
|
|
||||||
<div>
|
|
||||||
<b>
|
|
||||||
<span v-if="label" class="item-key"
|
|
||||||
:class="{'item-key-boolean-false': typeof value_internal === 'boolean' && !value_internal}"
|
|
||||||
>
|
|
||||||
"{{label}}"
|
|
||||||
</span>
|
|
||||||
:
|
|
||||||
</b>
|
|
||||||
</div>
|
|
||||||
<div v-if="typeof value_internal === 'boolean'"
|
|
||||||
:class="{'json-value-boolean-true': value_internal, 'json-value-boolean-false': !value_internal}"
|
|
||||||
@click="toggleBoolean"
|
|
||||||
>
|
|
||||||
<template v-if="value_internal">true</template>
|
|
||||||
<template v-else>false</template>
|
|
||||||
</div>
|
|
||||||
<div v-else
|
|
||||||
class="flex flex-row inline-edit"
|
|
||||||
:class="{
|
|
||||||
'json-value-number': typeof value_internal === 'number',
|
|
||||||
'json-value-string': typeof value_internal === 'string'
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<template v-if="typeof value_internal === 'string'">
|
|
||||||
"<div ref="element-edit-area"
|
|
||||||
contenteditable="true"
|
|
||||||
@keydown.enter="changeValue"
|
|
||||||
>
|
|
||||||
{{value_internal}}
|
|
||||||
</div>"
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<div ref="element-edit-area"
|
|
||||||
contenteditable="true"
|
|
||||||
@keydown.enter="changeValue"
|
|
||||||
>
|
|
||||||
{{value_internal}}
|
|
||||||
</div>
|
|
||||||
</template>,
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
name: 'json-element',
|
|
||||||
props: [
|
|
||||||
'value',
|
|
||||||
'label',
|
|
||||||
],
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
value_internal: undefined,
|
|
||||||
editing: false,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created() {
|
|
||||||
this.value_internal = this.value;
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
value: function(val) {
|
|
||||||
this.value_internal = val;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
toggleBoolean() {
|
|
||||||
this.value_internal = !this.value_internal;
|
|
||||||
this.$emit('change', this.value_internal);
|
|
||||||
},
|
|
||||||
changeValue() {
|
|
||||||
this.$refs['element-edit-area'].blur();
|
|
||||||
const newValue = this.$refs['element-edit-area'].textContent.replace(/\n/g, '');
|
|
||||||
if (isNaN(newValue)) {
|
|
||||||
this.value_internal = newValue;
|
|
||||||
this.$emit('change', newValue);
|
|
||||||
} else {
|
|
||||||
this.value_internal = +newValue;
|
|
||||||
this.$emit('change', +newValue);
|
|
||||||
}
|
|
||||||
this.editing = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped src="./json.scss">
|
|
||||||
</style>
|
|
||||||
@ -1,91 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="flex flex-column json-level-indent">
|
|
||||||
<div class="flex flex-row" @click="expanded_internal = !expanded_internal">
|
|
||||||
<div class="item-key-line">
|
|
||||||
<template v-if="label">
|
|
||||||
<b>
|
|
||||||
<span class="item-key">"{{label}}"</span>
|
|
||||||
:
|
|
||||||
</b>
|
|
||||||
</template>
|
|
||||||
<span v-if="!expanded_internal"><b> {</b> ... <b>}</b>,</span>
|
|
||||||
<template v-else><b>{</b></template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<template v-if="expanded_internal">
|
|
||||||
<div v-for="(row, rowKey) of value_internal"
|
|
||||||
:key="rowKey"
|
|
||||||
>
|
|
||||||
<template v-if="(ignoreKeys || {})[rowKey] !== true">
|
|
||||||
<JsonArray v-if="Array.isArray(row)"
|
|
||||||
:value="row"
|
|
||||||
:ignoreKeys="(ignoreKeys || {})[rowKey]"
|
|
||||||
@change="changeItem(rowKey, $event)"
|
|
||||||
>
|
|
||||||
</JsonArray>
|
|
||||||
<JsonObject v-else-if="typeof row === 'object' && row !== null"
|
|
||||||
:value="row"
|
|
||||||
:label="rowKey"
|
|
||||||
:ignoreKeys="(ignoreKeys || {})[rowKey]"
|
|
||||||
@change="changeItem(rowKey, $event)"
|
|
||||||
>
|
|
||||||
</JsonObject>
|
|
||||||
<JsonElement v-else
|
|
||||||
:value="row"
|
|
||||||
:label="rowKey"
|
|
||||||
@change="changeItem(rowKey, $event)"
|
|
||||||
>
|
|
||||||
</JsonElement>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
<div><b>},</b></div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import JsonArray from './JsonArray';
|
|
||||||
import JsonElement from './JsonElement';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'JsonObject',
|
|
||||||
props: [
|
|
||||||
'value',
|
|
||||||
'label',
|
|
||||||
'expanded',
|
|
||||||
'ignoreKeys',
|
|
||||||
],
|
|
||||||
components: {
|
|
||||||
JsonArray,
|
|
||||||
JsonElement,
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
value_internal: undefined,
|
|
||||||
expanded_internal: true,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created() {
|
|
||||||
this.value_internal = this.value;
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
value: function(val) {
|
|
||||||
this.value_internal = val;
|
|
||||||
},
|
|
||||||
expanded: function(val) {
|
|
||||||
if (val !== undefined && val !== null) {
|
|
||||||
this.expanded_internal = !!val;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
changeItem(key, value) {
|
|
||||||
this.value_internal[key] = value;
|
|
||||||
this.$emit('change', this.value_internal);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped src="./json.scss">
|
|
||||||
</style>
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
.uw-ultrawidify-container-root {
|
|
||||||
.json-level-indent {
|
|
||||||
padding-left: 2em !important;
|
|
||||||
font-family: 'Overpass Mono', monospace;
|
|
||||||
}
|
|
||||||
.item-key {
|
|
||||||
color: rgb(255, 196, 148);
|
|
||||||
}
|
|
||||||
.item-key-boolean-false {
|
|
||||||
color: rgb(207, 149, 101)
|
|
||||||
}
|
|
||||||
|
|
||||||
.json-value-boolean-true {
|
|
||||||
color: rgb(150, 240, 198);
|
|
||||||
}
|
|
||||||
.json-value-boolean-false {
|
|
||||||
color: rgb(241, 21, 21);
|
|
||||||
}
|
|
||||||
.json-value-number {
|
|
||||||
color: rgb(121, 121, 238);
|
|
||||||
}
|
|
||||||
.json-value-string {
|
|
||||||
color: rgb(226, 175, 7);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,28 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="root">
|
|
||||||
<template v-for="key in json"
|
|
||||||
>
|
|
||||||
<div class="element" :key="key">
|
|
||||||
<span class="json-key">"{{key}}"</span>:
|
|
||||||
<template v-if="Array.isArray(json[key])">
|
|
||||||
[<br/>
|
|
||||||
<div v-for="index of json[key]"
|
|
||||||
class="json-table-row"
|
|
||||||
:key="index"
|
|
||||||
>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
]
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
name: "json-explorer",
|
|
||||||
props: {
|
|
||||||
json: Object
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@ -1,55 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="qsb flex flex-row flex-cross-center flex-center flex-nogrow"
|
|
||||||
:class="{
|
|
||||||
'id': selector.startsWith('#'),
|
|
||||||
'class': selector.startsWith('.'),
|
|
||||||
}">
|
|
||||||
<div class="flex mt2px">{{selector}}</div> <span class="flex closeButton" @click="remove">×</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
props: {
|
|
||||||
selector: String,
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
remove() {
|
|
||||||
this.$emit('remove');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.uw-ultrawidify-container-root {
|
|
||||||
.mt2px {
|
|
||||||
margin-top: 7px;
|
|
||||||
}
|
|
||||||
.id {
|
|
||||||
border: 1px solid #d00;
|
|
||||||
background-color: rgba(216, 94, 24, 0.25)
|
|
||||||
}
|
|
||||||
.class {
|
|
||||||
border: 1px solid #33d;
|
|
||||||
background-color: rgba(69, 69, 255, 0.25)
|
|
||||||
}
|
|
||||||
.closeButton {
|
|
||||||
margin-top: 2px;
|
|
||||||
margin-left: 4px;
|
|
||||||
margin-right: 4px;
|
|
||||||
font-size: 1.5em;
|
|
||||||
color: #e00;
|
|
||||||
}
|
|
||||||
.closeButton:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
color: #fa6;
|
|
||||||
}
|
|
||||||
.qsb {
|
|
||||||
cursor:default;
|
|
||||||
margin-left: 3px;
|
|
||||||
margin-right: 3px;
|
|
||||||
margin-bottom: 2px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,87 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="flex flex-column">
|
|
||||||
<div v-if="!editing && !adding" class="flex flex-row">
|
|
||||||
<div class="">
|
|
||||||
<b>Query selector:</b> {{qs.string}}<br/>
|
|
||||||
<b>Additional CSS:</b> {{qs.css || 'no style rules'}}
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-column flex-nogrow">
|
|
||||||
<a @click="editing = true">Edit</a>
|
|
||||||
<a @click="$emit('delete')">Delete</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-else class="flex flex-row">
|
|
||||||
<div class="flex flex-column">
|
|
||||||
<div class="flex flex-row">
|
|
||||||
<div class="flex label-secondary form-label">
|
|
||||||
Query selector:
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-input">
|
|
||||||
<input type="text"
|
|
||||||
v-model="qs.string"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-row">
|
|
||||||
<div class="flex label-secondary form-label">
|
|
||||||
Additional CSS:
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-input">
|
|
||||||
<input type="text"
|
|
||||||
v-model="qs.css"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<a v-if="editing" @click="cancelEdit">Cancel</a>
|
|
||||||
<a v-if="adding" @click="clear">Clear</a>
|
|
||||||
<a @click="save">Save</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
qs: {string: '', css: ''},
|
|
||||||
editing: false,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
querySelector: Object,
|
|
||||||
adding: {
|
|
||||||
type: Boolean,
|
|
||||||
required: false,
|
|
||||||
default: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
querySelector(val) {
|
|
||||||
this.qs = JSON.parse(JSON.stringify(val));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
save() {
|
|
||||||
if (this.adding) {
|
|
||||||
this.$emit('create', this.qs);
|
|
||||||
this.qs = {string: '', css: ''};
|
|
||||||
} else {
|
|
||||||
this.emit$('update', this.qs)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
cancelEdit() {
|
|
||||||
this.qs = JSON.parse(JSON.stringify(this.querySelector));
|
|
||||||
},
|
|
||||||
clear() {
|
|
||||||
this.qs = {string: '', css: ''};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
|
|
||||||
</style>
|
|
||||||
@ -1,32 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="flex flex-column flex-center">
|
|
||||||
<div class="flex flex-self-center">
|
|
||||||
{{label}}
|
|
||||||
</div>
|
|
||||||
<div class="flex dark flex-self-center">
|
|
||||||
<small>
|
|
||||||
{{shortcut ? `(${shortcut})` : ''}}
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
props: {
|
|
||||||
label: String,
|
|
||||||
shortcut: String
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.uw-ultrawidify-container-root {
|
|
||||||
.center-text {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.dark {
|
|
||||||
opacity: 50%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,11 +1,15 @@
|
|||||||
enum AspectRatioType {
|
enum AspectRatioType {
|
||||||
Initial = -1, // page default
|
Default = -3, // inherit from global settings
|
||||||
Reset = 0, // reset to initial
|
Cycle = -2,
|
||||||
Automatic = 1, // set by Aard
|
Initial = -1, // page default
|
||||||
FitWidth = 2, // legacy/dynamic = fit to width
|
Reset = 0, // reset to initial
|
||||||
FitHeight = 3, // legacy/dynamic = fit to height
|
Automatic = 1, // we want to request automatic aspect ratio detection
|
||||||
Fixed = 4, // pre-determined aspect ratio
|
FitWidth = 2, // legacy/dynamic = fit to width
|
||||||
Manual = 5, // ratio achieved by zooming in/zooming out
|
FitHeight = 3, // legacy/dynamic = fit to height
|
||||||
|
Fixed = 4, // pre-determined aspect ratio
|
||||||
|
Manual = 5, // ratio achieved by zooming in/zooming out
|
||||||
|
AutomaticUpdate = 6, // set by Aard
|
||||||
|
Cover = 7, // replaces FitWidth and FitHeight
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AspectRatioType;
|
export default AspectRatioType;
|
||||||
|
|||||||
@ -0,0 +1,8 @@
|
|||||||
|
enum EmbeddedContentSettingsOverridePolicy {
|
||||||
|
Always = 2,
|
||||||
|
UseAsDefault = 1,
|
||||||
|
Default = 0,
|
||||||
|
Never = -1,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EmbeddedContentSettingsOverridePolicy;
|
||||||
@ -1,10 +1,9 @@
|
|||||||
enum ExtensionMode {
|
enum ExtensionMode {
|
||||||
AutoDisabled = -2,
|
|
||||||
Disabled = -1,
|
Disabled = -1,
|
||||||
Default = 0,
|
Default = 0,
|
||||||
Whitelist = 1,
|
FullScreen = 1,
|
||||||
Basic = 2,
|
Theater = 2,
|
||||||
Enabled = 3,
|
All = 3
|
||||||
};
|
}
|
||||||
|
|
||||||
export default ExtensionMode;
|
export default ExtensionMode;
|
||||||
|
|||||||
5
src/common/enums/InputHandlingMode.enum.ts
Normal file
5
src/common/enums/InputHandlingMode.enum.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export enum InputHandlingMode {
|
||||||
|
Disabled = -1,
|
||||||
|
Default = 0,
|
||||||
|
Enabled = 1, // enable, but don't override site shortcuts
|
||||||
|
}
|
||||||
10
src/common/enums/LegacyExtensionMode.enum.ts
Normal file
10
src/common/enums/LegacyExtensionMode.enum.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
enum LegacyExtensionMode {
|
||||||
|
AutoDisabled = -2,
|
||||||
|
Disabled = -1,
|
||||||
|
Default = 0,
|
||||||
|
Whitelist = 1,
|
||||||
|
Basic = 2,
|
||||||
|
Enabled = 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LegacyExtensionMode;
|
||||||
5
src/common/enums/PlayerDetectionMode.enum.ts
Normal file
5
src/common/enums/PlayerDetectionMode.enum.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export enum PlayerDetectionMode {
|
||||||
|
Auto = 0,
|
||||||
|
AncestorIndex = 1,
|
||||||
|
QuerySelectors = 2,
|
||||||
|
};
|
||||||
9
src/common/enums/SiteSupportLevel.enum.ts
Normal file
9
src/common/enums/SiteSupportLevel.enum.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export enum SiteSupportLevel {
|
||||||
|
OfficialSupport = 'official',
|
||||||
|
CommunitySupport = 'community',
|
||||||
|
UserDefined = 'user-defined',
|
||||||
|
BetaSupport = 'testing',
|
||||||
|
OfficialBlacklist = 'officially-disabled',
|
||||||
|
Unknown = 'unknown',
|
||||||
|
UserModified = 'modified',
|
||||||
|
}
|
||||||
@ -2,7 +2,10 @@ enum VideoAlignmentType {
|
|||||||
Left = 0,
|
Left = 0,
|
||||||
Center = 1,
|
Center = 1,
|
||||||
Right = 2,
|
Right = 2,
|
||||||
Default = -1
|
Top = 3,
|
||||||
|
Bottom = 4,
|
||||||
|
Default = -1,
|
||||||
|
Custom = -2,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default VideoAlignmentType;
|
export default VideoAlignmentType;
|
||||||
|
|||||||
13
src/common/interfaces/ArInterface.ts
Normal file
13
src/common/interfaces/ArInterface.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import AspectRatioType from '../enums/AspectRatioType.enum';
|
||||||
|
|
||||||
|
export enum ArVariant {
|
||||||
|
Crop = undefined,
|
||||||
|
Zoom = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Ar {
|
||||||
|
type: AspectRatioType,
|
||||||
|
ratio?: number,
|
||||||
|
variant?: ArVariant,
|
||||||
|
offset?: number,
|
||||||
|
}
|
||||||
31
src/common/interfaces/ClientUiMenu.interface.ts
Normal file
31
src/common/interfaces/ClientUiMenu.interface.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { CommandInterface, InPlayerUIOptions } from '@src/common/interfaces/SettingsInterface';
|
||||||
|
|
||||||
|
export interface MenuItemBaseConfig {
|
||||||
|
label?: string;
|
||||||
|
subitems?: MenuItemConfig[];
|
||||||
|
command?: CommandInterface,
|
||||||
|
action?: () => void;
|
||||||
|
customHTML?: HTMLElement | string;
|
||||||
|
customId?: string;
|
||||||
|
customClassList?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MenuItemConfig = MenuItemBaseConfig & ({label: string} | {customHTML: HTMLElement | string});
|
||||||
|
|
||||||
|
export enum MenuPosition {
|
||||||
|
TopLeft = 'top-left',
|
||||||
|
Left = 'left',
|
||||||
|
BottomLeft = 'bottom-left',
|
||||||
|
Top = 'top',
|
||||||
|
Bottom = 'bottom',
|
||||||
|
TopRight = 'top-right',
|
||||||
|
Right = 'right',
|
||||||
|
BottomRight = 'bottom-right',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MenuConfig {
|
||||||
|
isGlobal?: boolean;
|
||||||
|
ui: InPlayerUIOptions;
|
||||||
|
options?: {forceShow?: boolean};
|
||||||
|
items: MenuItemConfig[];
|
||||||
|
}
|
||||||
43
src/common/interfaces/EventBusMessage.interface.ts
Normal file
43
src/common/interfaces/EventBusMessage.interface.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { CommsOrigin } from '@/ext/module/comms/CommsClient';
|
||||||
|
import type { Runtime } from 'chrome';
|
||||||
|
|
||||||
|
export interface EventBusCommand {
|
||||||
|
isGlobal?: boolean,
|
||||||
|
source?: any,
|
||||||
|
function: (commandData: any, context?: EventBusContext) => void | Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EventBusMessage<T = unknown> {
|
||||||
|
command: string;
|
||||||
|
config?: T;
|
||||||
|
context?: EventBusContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EventBusContext {
|
||||||
|
stopPropagation?: boolean;
|
||||||
|
|
||||||
|
origin?: CommsOrigin;
|
||||||
|
frameUrl?: string;
|
||||||
|
|
||||||
|
// tab?: number;
|
||||||
|
// frame?: number;
|
||||||
|
// port?: string;
|
||||||
|
|
||||||
|
visitedBusses?: string[];
|
||||||
|
commandId?: string;
|
||||||
|
|
||||||
|
comms?: {
|
||||||
|
forwardTo?: 'all' | 'active' | 'popup' | 'contentScript' | 'all-frames';
|
||||||
|
sender?: Runtime.MessageSender;
|
||||||
|
port?: Runtime.Port;
|
||||||
|
// frame?: any;
|
||||||
|
sourceFrame?: {
|
||||||
|
tabId: number;
|
||||||
|
frameId: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
borderCrossings?: {
|
||||||
|
commsServer?: boolean,
|
||||||
|
iframe?: boolean,
|
||||||
|
};
|
||||||
|
}
|
||||||
8
src/common/interfaces/HostData.interface.ts
Normal file
8
src/common/interfaces/HostData.interface.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { ExtensionEnvironment } from '@src/common/interfaces/SettingsInterface';
|
||||||
|
|
||||||
|
export interface HostInfo {
|
||||||
|
host: string,
|
||||||
|
hasVideo: boolean,
|
||||||
|
minEnvironment: ExtensionEnvironment,
|
||||||
|
maxEnvironment: ExtensionEnvironment
|
||||||
|
};
|
||||||
6
src/common/interfaces/IframeTunnelPayload.interface.ts
Normal file
6
src/common/interfaces/IframeTunnelPayload.interface.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { EventBusMessage } from '@src/common/interfaces/EventBusMessage.interface';
|
||||||
|
|
||||||
|
export interface IframeTunnelPayload {
|
||||||
|
action: string,
|
||||||
|
payload: EventBusMessage,
|
||||||
|
}
|
||||||
19
src/common/interfaces/ScalingParamsBroadcast.interface.ts
Normal file
19
src/common/interfaces/ScalingParamsBroadcast.interface.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import VideoAlignmentType from '@src/common/enums/VideoAlignmentType.enum';
|
||||||
|
import { Ar } from '@src/common/interfaces/ArInterface';
|
||||||
|
import { Stretch } from '@src/common/interfaces/StretchInterface';
|
||||||
|
|
||||||
|
export interface ScalingParamsBroadcast {
|
||||||
|
effectiveZoom: {
|
||||||
|
x: number,
|
||||||
|
y: number
|
||||||
|
},
|
||||||
|
videoAlignment: {
|
||||||
|
x: VideoAlignmentType,
|
||||||
|
y: VideoAlignmentType,
|
||||||
|
xPos?: number,
|
||||||
|
yPos?: number,
|
||||||
|
}
|
||||||
|
lastAr: Ar,
|
||||||
|
manualZoom: boolean,
|
||||||
|
stretch: Stretch,
|
||||||
|
}
|
||||||
@ -1,160 +1,370 @@
|
|||||||
import { Action } from '../../../node_modules/vuex/types/index'
|
import { AardPollingOptions } from '@src/ext/module/aard/enums/aard-polling-options.enum'
|
||||||
import AntiGradientMode from '../enums/AntiGradientMode.enum'
|
import { AardSubtitleCropMode } from '@src/ext/module/aard/enums/aard-subtitle-crop-mode.enum'
|
||||||
import AspectRatioType from '../enums/AspectRatioType.enum'
|
import { SiteSupportLevel } from '@src/common/enums/SiteSupportLevel.enum';
|
||||||
import CropModePersistence from '../enums/CropModePersistence.enum'
|
import AspectRatioType from '@src/common/enums/AspectRatioType.enum'
|
||||||
import ExtensionMode from '../enums/ExtensionMode.enum'
|
import CropModePersistence from '@src/common/enums/CropModePersistence.enum'
|
||||||
import StretchType from '../enums/StretchType.enum'
|
import EmbeddedContentSettingsOverridePolicy from '@src/common/enums/EmbeddedContentSettingsOverridePolicy.enum'
|
||||||
import VideoAlignmentType from '../enums/VideoAlignmentType.enum'
|
import ExtensionMode from '@src/common/enums/ExtensionMode.enum'
|
||||||
|
import StretchType from '@src/common/enums/StretchType.enum'
|
||||||
|
import VideoAlignmentType from '@src/common/enums/VideoAlignmentType.enum'
|
||||||
|
import { PlayerDetectionMode } from '@src/common/enums/PlayerDetectionMode.enum';
|
||||||
|
import { InputHandlingMode } from '@src/common/enums/InputHandlingMode.enum';
|
||||||
|
import { MenuPosition } from '@src/common/interfaces/ClientUiMenu.interface';
|
||||||
|
|
||||||
|
export enum ExtensionEnvironment {
|
||||||
|
Normal = ExtensionMode.All,
|
||||||
|
Theater = ExtensionMode.Theater,
|
||||||
|
Fullscreen = ExtensionMode.FullScreen,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DevUiConfig {
|
||||||
|
aardDebugOverlay: {
|
||||||
|
showOnStartup: boolean,
|
||||||
|
showDetectionDetails: boolean,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface KeyboardShortcutInterface {
|
||||||
|
key?: string,
|
||||||
|
code?: string,
|
||||||
|
ctrlKey?: boolean,
|
||||||
|
metaKey?: boolean,
|
||||||
|
altKey?: boolean,
|
||||||
|
shiftKey?: boolean,
|
||||||
|
onKeyUp?: boolean,
|
||||||
|
onKeyDown?: boolean,
|
||||||
|
onMouseMove?: boolean,
|
||||||
|
}
|
||||||
|
|
||||||
interface ActionScopeInterface {
|
interface ActionScopeInterface {
|
||||||
show: boolean,
|
show: boolean,
|
||||||
label?: string, // example override, takes precedence over default label
|
label?: string, // example override, takes precedence over default label
|
||||||
shortcut?: {
|
shortcut?: KeyboardShortcutInterface[],
|
||||||
key?: string,
|
|
||||||
code?: string,
|
|
||||||
ctrlKey?: boolean,
|
|
||||||
metaKey?: boolean,
|
|
||||||
altKey?: boolean,
|
|
||||||
shiftKey?: boolean,
|
|
||||||
onKeyUp?: boolean,
|
|
||||||
onKeyDown?: boolean,
|
|
||||||
onMouseMove?: boolean,
|
|
||||||
}[],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SettingsInterface {
|
interface RestrictionsSettings {
|
||||||
arDetect: {
|
disableOnSmallPlayers?: boolean; // Whether ultrawidify should disable itself when the player is small
|
||||||
disabledReason: string, // if automatic aspect ratio has been disabled, show reason
|
minAllowedWidth?: number; // if player is less than this many px wide, ultrawidify will disable itself
|
||||||
allowedMisaligned: number, // top and bottom letterbox thickness can differ by this much.
|
minAllowedHeight?: number; // if player is less than this many px tall, ultrawidify will disable itself
|
||||||
// Any more and we don't adjust ar.
|
onlyAllowInFullscreen?: boolean; // if enabled, ultrawidify will be disabled when not in full screen regardless of what previous two options say
|
||||||
allowedArVariance: number, // amount by which old ar can differ from the new (1 = 100%)
|
onlyAllowAutodetectionInFullScreen?: boolean; // if enabled, autodetection will only start once in full screen
|
||||||
timers: { // autodetection frequency
|
}
|
||||||
playing: number, // while playing
|
|
||||||
paused: number, // while paused
|
|
||||||
error: number, // after error
|
|
||||||
minimumTimeout: number,
|
|
||||||
tickrate: number, // 1 tick every this many milliseconds
|
|
||||||
},
|
|
||||||
autoDisable: { // settings for automatically disabling the extension
|
|
||||||
maxExecutionTime: number, // if execution time of main autodetect loop exceeds this many milliseconds,
|
|
||||||
// we disable it.
|
|
||||||
consecutiveTimeoutCount: number, // we only do it if it happens this many consecutive times
|
|
||||||
|
|
||||||
// FOR FUTURE USE
|
export interface CommandInterface {
|
||||||
consecutiveArResets: number // if aspect ratio reverts immediately after AR change is applied, we disable everything
|
action: string,
|
||||||
},
|
label: string,
|
||||||
canvasDimensions: {
|
comment?: string,
|
||||||
blackframeCanvas: { // smaller than sample canvas, blackframe canvas is used to recon for black frames
|
arguments?: any,
|
||||||
// it's not used to detect aspect ratio by itself, so it can be tiny af
|
shortcut?: KeyboardShortcutInterface,
|
||||||
width: number,
|
internalOnly?: boolean,
|
||||||
height: number,
|
actionId?: string,
|
||||||
},
|
}
|
||||||
sampleCanvas: { // size of image sample for detecting aspect ratio. Bigger size means more accurate results,
|
|
||||||
// at the expense of performance
|
|
||||||
width: number,
|
|
||||||
height: number,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
// samplingInterval: 10, // we sample at columns at (width/this) * [ 1 .. this - 1]
|
export type SettingsReloadComponent = 'PlayerData' | 'VideoData';
|
||||||
blackframe: {
|
export type SettingsReloadFlags = true | SettingsReloadComponent;
|
||||||
sufficientColorVariance: number, // calculate difference between average intensity and pixel, for every pixel for every color
|
|
||||||
// component. Average intensity is normalized to where 0 is black and 1 is biggest value for
|
export interface AardSubtitleScanOptions {
|
||||||
// that component. If sum of differences between normalized average intensity and normalized
|
subtitleCropMode: AardSubtitleCropMode,
|
||||||
// component varies more than this % between color components, we can afford to use less strict
|
resumeAfter: number, // resume after X ms of no subtitle
|
||||||
// cumulative threshold.
|
scanSpacing: number, // repeat subtitle scan every _n_ lines
|
||||||
cumulativeThresholdLax: number,
|
scanMargin: number, // % of pixels (from edge to start area) that we skip while scanning.
|
||||||
cumulativeThresholdStrict: number,// if we add values of all pixels together and get more than this, the frame is bright enough.
|
// While technically anything between 0 and 0.5 is valid, the value
|
||||||
// (note: blackframe is 16x9 px -> 144px total. cumulative threshold can be reached fast)
|
// should be somewhere between 0.1-0.3 in order for subtitle scan to
|
||||||
blackPixelsCondition: number, // How much pixels must be black (1 all, 0 none) before we consider frame as black. Takes
|
// work properly.
|
||||||
// precedence over cumulative threshold: if blackPixelsCondition is met, the frame is dark
|
maxValidLetter: number, // if letter is longer than this, something's off.
|
||||||
// regardless of whether cumulative threshold has been reached.
|
subtitleSubpixelThresholdOn: number,
|
||||||
},
|
subtitleSubpixelThresholdOff: number,
|
||||||
blackbar: {
|
|
||||||
blackLevel: number, // everything darker than 10/255 across all RGB components is considered black by
|
minDetections: number,
|
||||||
// default. blackLevel can decrease if we detect darker black.
|
minImageLineDetections: number,
|
||||||
threshold: number, // if pixel is darker than the sum of black level and this value, we count it as black
|
|
||||||
// on 0-255. Needs to be fairly high (8 might not cut it) due to compression
|
maxPotentialSubtitleMisalignment: number, // how many pixels off-center can "potential subtitle" be
|
||||||
// artifacts in the video itself
|
|
||||||
frameThreshold: number, // threshold, but when doing blackframe test
|
refiningScanSpacing: number, // must be base-2
|
||||||
imageThreshold: number, // in order to detect pixel as "not black", the pixel must be brighter than
|
refiningScanInitialIterations: number,
|
||||||
// the sum of black level, threshold and this value.
|
}
|
||||||
gradientThreshold: number, // When trying to determine thickness of the black bars, we take 2 values: position of
|
|
||||||
// the last pixel that's darker than our threshold, and position of the first pixel that's
|
export interface AardSettings {
|
||||||
// brighter than our image threshold. If positions are more than this many pixels apart,
|
aardType: 'webgl' | 'legacy' | 'auto';
|
||||||
// we assume we aren't looking at letterbox and thus don't correct the aspect ratio.
|
useLegacy: boolean,
|
||||||
gradientSampleSize: number, // How far do we look to find the gradient
|
|
||||||
maxGradient: number, // if two neighboring pixels in gradientSampleSize differ by more than this, then we aren't
|
earlyStopOptions: {
|
||||||
// looking at a gradient
|
stopAfterFirstDetection: boolean;
|
||||||
gradientNegativeTreshold: number,
|
stopAfterTimeout: boolean;
|
||||||
gradientMaxSD: number, // reserved for future use
|
stopTimeout: number;
|
||||||
antiGradientMode: AntiGradientMode
|
|
||||||
},
|
|
||||||
variableBlackbarThresholdOptions: { // In case of poor bitrate videos, jpeg artifacts may cause us issues
|
|
||||||
// FOR FUTURE USE
|
|
||||||
enabled: boolean, // allow increasing blackbar threshold
|
|
||||||
disableArDetectOnMax: boolean, // disable autodetection when threshold goes over max blackbar threshold
|
|
||||||
maxBlackbarThreshold: number, // max threshold (don't increase past this)
|
|
||||||
thresholdStep: number, // when failing to set aspect ratio, increase threshold by this much
|
|
||||||
increaseAfterConsecutiveResets: number // increase if AR resets this many times in a row
|
|
||||||
},
|
|
||||||
sampling: {
|
|
||||||
staticCols: number, // we take a column at [0-n]/n-th parts along the width and sample it
|
|
||||||
randomCols: number, // we add this many randomly selected columns to the static columns
|
|
||||||
staticRows: number, // forms grid with staticSampleCols. Determined in the same way. For black frame checks
|
|
||||||
},
|
|
||||||
guardLine: { // all pixels on the guardline need to be black, or else we trigger AR recalculation
|
|
||||||
// (if AR fails to be recalculated, we reset AR)
|
|
||||||
enabled: boolean,
|
|
||||||
ignoreEdgeMargin: number, // we ignore anything that pokes over the black line this close to the edge
|
|
||||||
// (relative to width of the sample)
|
|
||||||
imageTestThreshold: number, // when testing for image, this much pixels must be over blackbarThreshold
|
|
||||||
edgeTolerancePx: number, // black edge violation is performed this far from reported 'last black pixel'
|
|
||||||
edgeTolerancePercent: null // unused. same as above, except use % of canvas height instead of pixels
|
|
||||||
},
|
|
||||||
fallbackMode: {
|
|
||||||
enabled: boolean,
|
|
||||||
safetyBorderPx: number, // determines the thickness of safety border in fallback mode
|
|
||||||
noTriggerZonePx: number // if we detect edge less than this many pixels thick, we don't correct.
|
|
||||||
},
|
|
||||||
arSwitchLimiter: { // to be implemented
|
|
||||||
switches: number, // we can switch this many times
|
|
||||||
period: number // per this period
|
|
||||||
},
|
|
||||||
edgeDetection: {
|
|
||||||
sampleWidth: number, // we take a sample this wide for edge detection
|
|
||||||
detectionThreshold: number, // sample needs to have this many non-black pixels to be a valid edge
|
|
||||||
confirmationThreshold: number, //
|
|
||||||
singleSideConfirmationThreshold: number, // we need this much edges (out of all samples, not just edges) in order
|
|
||||||
// to confirm an edge in case there's no edges on top or bottom (other
|
|
||||||
// than logo, of course)
|
|
||||||
logoThreshold: number, // if edge candidate sits with count greater than this*all_samples, it can't be logo
|
|
||||||
// or watermark.
|
|
||||||
edgeTolerancePx?: number, // we check for black edge violation this far from detection point
|
|
||||||
edgeTolerancePercent?: number, // we check for black edge detection this % of height from detection point. unused
|
|
||||||
middleIgnoredArea: number, // we ignore this % of canvas height towards edges while detecting aspect ratios
|
|
||||||
minColsForSearch: number, // if we hit the edge of blackbars for all but this many columns (%-wise), we don't
|
|
||||||
// continue with search. It's pointless, because black edge is higher/lower than we
|
|
||||||
// are now. (NOTE: keep this less than 1 in case we implement logo detection)
|
|
||||||
},
|
|
||||||
pillarTest: {
|
|
||||||
ignoreThinPillarsPx: number, // ignore pillars that are less than this many pixels thick.
|
|
||||||
allowMisaligned: number // left and right edge can vary this much (%)
|
|
||||||
},
|
|
||||||
textLineTest: {
|
|
||||||
nonTextPulse: number, // if a single continuous pulse has this many non-black pixels, we aren't dealing
|
|
||||||
// with text. This value is relative to canvas width (%)
|
|
||||||
pulsesToConfirm: number, // this is a threshold to confirm we're seeing text.
|
|
||||||
pulsesToConfirmIfHalfBlack: number, // this is the threshold to confirm we're seeing text if longest black pulse
|
|
||||||
// is over 50% of the canvas width
|
|
||||||
testRowOffset: number // we test this % of height from detected edge
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
polling: {
|
||||||
|
runInBackgroundTabs: AardPollingOptions;
|
||||||
|
runOnSmallVideos: AardPollingOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
disabledReason: string, // if automatic aspect ratio has been disabled, show reason
|
||||||
|
allowedMisaligned: number, // top and bottom letterbox thickness can differ by this much.
|
||||||
|
// Any more and we don't adjust ar.
|
||||||
|
allowedArVariance: number, // amount by which old ar can differ from the new (1 = 100%)
|
||||||
|
timers: { // autodetection frequency
|
||||||
|
playing: number, // while playing
|
||||||
|
playingReduced: number, // while video/player element has insufficient size
|
||||||
|
paused: number, // while paused
|
||||||
|
error: number, // after error
|
||||||
|
minimumTimeout: number,
|
||||||
|
tickrate: number, // 1 tick every this many milliseconds
|
||||||
|
},
|
||||||
|
|
||||||
|
subtitles: AardSubtitleScanOptions,
|
||||||
|
|
||||||
|
autoDisable: { // settings for automatically disabling the extension
|
||||||
|
onFirstChange: boolean,
|
||||||
|
ifNotChanged: boolean,
|
||||||
|
ifNotChangedTimeout: number,
|
||||||
|
ifSubtitles: boolean;
|
||||||
|
},
|
||||||
|
canvasDimensions: {
|
||||||
|
blackframeCanvas: { // smaller than sample canvas, blackframe canvas is used to recon for black frames
|
||||||
|
// it's not used to detect aspect ratio by itself, so it can be tiny af
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
},
|
||||||
|
sampleCanvas: { // size of image sample for detecting aspect ratio. Bigger size means more accurate results,
|
||||||
|
// at the expense of performance
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
blackLevels: {
|
||||||
|
defaultBlack: number, // By default, pixels darker than this are considered black.
|
||||||
|
// (If detection algorithm detects darker blacks, black is considered darkest detected pixel)
|
||||||
|
blackTolerance: number, // If pixel is more than this much brighter than blackLevel, it's considered not black
|
||||||
|
// It is not considered a valid image detection if gradient detection is enabled
|
||||||
|
imageDelta: number, // When gradient detection is enabled, pixels this much brighter than black skip gradient detection
|
||||||
|
}
|
||||||
|
sampling: {
|
||||||
|
edgePosition: number; // % of width (max 0.33). Pixels up to this far away from either edge may contain logo.
|
||||||
|
staticCols: number, // we take a column at [0-n]/n-th parts along the width and sample it
|
||||||
|
randomCols: number, // we add this many randomly selected columns to the static columns
|
||||||
|
staticRows: number, // forms grid with staticSampleCols. Determined in the same way. For black frame checks,
|
||||||
|
},
|
||||||
|
|
||||||
|
// pls deprecate and move things used
|
||||||
|
edgeDetection: {
|
||||||
|
gradientThreshold: number, // if more than this percentage (0-1) is detected as gradient, we mark edge as gradient
|
||||||
|
gradientTestMinDelta: number, // if difference between test row and before row is MORE than this -> not gradient
|
||||||
|
gradientTestMinDeltaAfter: number, // if difference between test row and after row is LESS than this -> not gradient
|
||||||
|
gradientTestMaxDeltaAfter: number, // if difference between test row and after row is MORE than this -> not gradient
|
||||||
|
maxLetterboxOffset: number, // Upper and lower letterbox can be different by this many (% of height)
|
||||||
|
|
||||||
|
minValidImage: number, // if more than this % (0-1) of row is image, we confirm image regardless of other criteria except gradient
|
||||||
|
maxEdgeSegments: number, // if edge has more than this many segments, we consider it unreliable
|
||||||
|
minEdgeSegmentSize: number,
|
||||||
|
averageEdgeThreshold: number, // average edge must be this many px
|
||||||
|
},
|
||||||
|
|
||||||
|
letterboxOrientationScan: {
|
||||||
|
letterboxLimit: number, // how many non-black pixels we can detect before ruling out letterbox
|
||||||
|
pillarboxLimit: number, // how many non-black pixels we can detect before ruling out pillarbox
|
||||||
|
}
|
||||||
|
|
||||||
|
pillarTest: {
|
||||||
|
ignoreThinPillarsPx: number, // ignore pillars that are less than this many pixels thick.
|
||||||
|
allowMisaligned: number // left and right edge can vary this much (%)
|
||||||
|
},
|
||||||
|
textLineTest: {
|
||||||
|
nonTextPulse: number, // if a single continuous pulse has this many non-black pixels, we aren't dealing
|
||||||
|
// with text. This value is relative to canvas width (%)
|
||||||
|
pulsesToConfirm: number, // this is a threshold to confirm we're seeing text.
|
||||||
|
pulsesToConfirmIfHalfBlack: number, // this is the threshold to confirm we're seeing text if longest black pulse
|
||||||
|
// is over 50% of the canvas width
|
||||||
|
testRowOffset: number // we test this % of height from detected edge
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AardLegacySettings {
|
||||||
|
aardType: 'webgl' | 'legacy' | 'auto';
|
||||||
|
|
||||||
|
earlyStopOptions: {
|
||||||
|
stopAfterFirstDetection: boolean;
|
||||||
|
stopAfterTimeout: boolean;
|
||||||
|
stopTimeout: number;
|
||||||
|
},
|
||||||
|
|
||||||
|
polling: {
|
||||||
|
runInBackgroundTabs: AardPollingOptions;
|
||||||
|
runOnSmallVideos: AardPollingOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
disabledReason: string, // if automatic aspect ratio has been disabled, show reason
|
||||||
|
allowedMisaligned: number, // top and bottom letterbox thickness can differ by this much.
|
||||||
|
// Any more and we don't adjust ar.
|
||||||
|
allowedArVariance: number, // amount by which old ar can differ from the new (1 = 100%)
|
||||||
|
timers: { // autodetection frequency
|
||||||
|
playing: number, // while playing
|
||||||
|
playingReduced: number, // while video/player element has insufficient size
|
||||||
|
paused: number, // while paused
|
||||||
|
error: number, // after error
|
||||||
|
minimumTimeout: number,
|
||||||
|
tickrate: number, // 1 tick every this many milliseconds
|
||||||
|
},
|
||||||
|
|
||||||
|
subtitles: AardSubtitleScanOptions,
|
||||||
|
|
||||||
|
autoDisable: { // settings for automatically disabling the extension
|
||||||
|
onFirstChange: boolean,
|
||||||
|
ifNotChanged: boolean,
|
||||||
|
ifNotChangedTimeout: number,
|
||||||
|
ifSubtitles: boolean;
|
||||||
|
},
|
||||||
|
canvasDimensions: {
|
||||||
|
blackframeCanvas: { // smaller than sample canvas, blackframe canvas is used to recon for black frames
|
||||||
|
// it's not used to detect aspect ratio by itself, so it can be tiny af
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
},
|
||||||
|
sampleCanvas: { // size of image sample for detecting aspect ratio. Bigger size means more accurate results,
|
||||||
|
// at the expense of performance
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
blackLevels: {
|
||||||
|
defaultBlack: number, // By default, pixels darker than this are considered black.
|
||||||
|
// (If detection algorithm detects darker blacks, black is considered darkest detected pixel)
|
||||||
|
blackTolerance: number, // If pixel is more than this much brighter than blackLevel, it's considered not black
|
||||||
|
// It is not considered a valid image detection if gradient detection is enabled
|
||||||
|
imageDelta: number, // When gradient detection is enabled, pixels this much brighter than black skip gradient detection
|
||||||
|
}
|
||||||
|
sampling: {
|
||||||
|
edgePosition: number; // % of width (max 0.33). Pixels up to this far away from either edge may contain logo.
|
||||||
|
staticCols: number, // we take a column at [0-n]/n-th parts along the width and sample it
|
||||||
|
randomCols: number, // we add this many randomly selected columns to the static columns
|
||||||
|
staticRows: number, // forms grid with staticSampleCols. Determined in the same way. For black frame checks,
|
||||||
|
},
|
||||||
|
|
||||||
|
// pls deprecate and move things used
|
||||||
|
edgeDetection: {
|
||||||
|
slopeTestWidth: number,
|
||||||
|
gradientTestSamples: number, // we check this many pixels below (or above) the suspected edge to check for gradient
|
||||||
|
gradientTestBlackThreshold: number, // if pixel in test sample is brighter than that, we aren't looking at gradient
|
||||||
|
gradientTestDeltaThreshold: number, // if delta between two adjacent pixels in gradient test exceeds this, it's not gradient
|
||||||
|
|
||||||
|
thresholds: {
|
||||||
|
edgeDetectionLimit: number, // during scanning of the edge, quit after edge gets detected at this many points
|
||||||
|
minQualitySingleEdge: number, // At least one of the detected must reach this quality
|
||||||
|
minQualitySecondEdge: number, // The other edge must reach this quality (must be smaller or equal to single edge quality)
|
||||||
|
}
|
||||||
|
|
||||||
|
gradientThreshold: number, // if more than this percentage (0-1) is detected as gradient, we mark edge as gradient
|
||||||
|
gradientTestMinDelta: number, // if difference between test row and before row is MORE than this -> not gradient
|
||||||
|
gradientTestMinDeltaAfter: number, // if difference between test row and after row is LESS than this -> not gradient
|
||||||
|
gradientTestMaxDeltaAfter: number, // if difference between test row and after row is MORE than this -> not gradient
|
||||||
|
maxLetterboxOffset: number, // Upper and lower letterbox can be different by this many (% of height)
|
||||||
|
|
||||||
|
// Previous iteration variables VVVV
|
||||||
|
sampleWidth: number, // we take a sample this wide for edge detection
|
||||||
|
detectionThreshold: number, // sample needs to have this many non-black pixels to be a valid edge
|
||||||
|
confirmationThreshold: number, //
|
||||||
|
singleSideConfirmationThreshold: number, // we need this much edges (out of all samples, not just edges) in order
|
||||||
|
// to confirm an edge in case there's no edges on top or bottom (other
|
||||||
|
// than logo, of course)
|
||||||
|
logoThreshold: number, // if edge candidate sits with count greater than this*all_samples, it can't be logo
|
||||||
|
// or watermark.
|
||||||
|
edgeTolerancePx?: number, // we check for black edge violation this far from detection point
|
||||||
|
edgeTolerancePercent?: number, // we check for black edge detection this % of height from detection point. unused
|
||||||
|
middleIgnoredArea: number, // we ignore this % of canvas height towards edges while detecting aspect ratios
|
||||||
|
minColsForSearch: number, // if we hit the edge of blackbars for all but this many columns (%-wise), we don't
|
||||||
|
// continue with search. It's pointless, because black edge is higher/lower than we
|
||||||
|
// are now. (NOTE: keep this less than 1 in case we implement logo detection)
|
||||||
|
edgeMismatchTolerancePx: number,// corners and center are considered equal if they differ by at most this many px
|
||||||
|
|
||||||
|
|
||||||
|
minValidImage: number, // if more than this % (0-1) of row is image, we confirm image regardless of other criteria except gradient
|
||||||
|
maxEdgeSegments: number, // if edge has more than this many segments, we consider it unreliable
|
||||||
|
minEdgeSegmentSize: number,
|
||||||
|
averageEdgeThreshold: number, // average edge must be this many px
|
||||||
|
},
|
||||||
|
|
||||||
|
letterboxOrientationScan: {
|
||||||
|
letterboxLimit: number, // how many non-black pixels we can detect before ruling out letterbox
|
||||||
|
pillarboxLimit: number, // how many non-black pixels we can detect before ruling out pillarbox
|
||||||
|
}
|
||||||
|
|
||||||
|
pillarTest: {
|
||||||
|
ignoreThinPillarsPx: number, // ignore pillars that are less than this many pixels thick.
|
||||||
|
allowMisaligned: number // left and right edge can vary this much (%)
|
||||||
|
},
|
||||||
|
textLineTest: {
|
||||||
|
nonTextPulse: number, // if a single continuous pulse has this many non-black pixels, we aren't dealing
|
||||||
|
// with text. This value is relative to canvas width (%)
|
||||||
|
pulsesToConfirm: number, // this is a threshold to confirm we're seeing text.
|
||||||
|
pulsesToConfirmIfHalfBlack: number, // this is the threshold to confirm we're seeing text if longest black pulse
|
||||||
|
// is over 50% of the canvas width
|
||||||
|
testRowOffset: number // we test this % of height from detected edge
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
interface DevSettings {
|
||||||
|
loadFromSnapshot: boolean,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InPlayerUIOptions {
|
||||||
|
activatorAlignment: MenuPosition,
|
||||||
|
minEnabledWidth: number, // don't show UI if player is narrower than % of screen width
|
||||||
|
minEnabledHeight: number, // don't show UI if player is narrower than % of screen height
|
||||||
|
activation: 'player' | 'trigger-zone' | 'distance' | 'none', // what needs to be hovered in order for UI to be visible
|
||||||
|
activateWithCtrl: boolean,
|
||||||
|
activationDistance: number,
|
||||||
|
activationDistanceUnits: '%' | 'px',
|
||||||
|
activatorPadding: {x: number, y: number}
|
||||||
|
activatorPaddingUnit: {x: '%' | 'px', y: '%' | 'px'},
|
||||||
|
triggerZoneDimensions: { // how large the trigger zone is (relative to player size)
|
||||||
|
width: number
|
||||||
|
height: number,
|
||||||
|
offsetX: number, // fed to translateX(offsetX + '%'). Valid range [-100, 0]
|
||||||
|
offsetY: number // fed to translateY(offsetY + '%'). Valid range [-100, 100]
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
interface SettingsInterface {
|
||||||
|
_updateFlags?: {
|
||||||
|
requireReload?: SettingsReloadFlags,
|
||||||
|
forSite?: string
|
||||||
|
}
|
||||||
|
dev: DevSettings,
|
||||||
|
|
||||||
|
aardLegacy: AardLegacySettings,
|
||||||
|
aard: AardSettings,
|
||||||
|
|
||||||
|
ui: {
|
||||||
|
inPlayer: InPlayerUIOptions,
|
||||||
|
devMode?: boolean,
|
||||||
|
dev: DevUiConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
restrictions?: RestrictionsSettings;
|
||||||
|
|
||||||
|
crop: {
|
||||||
|
default: any;
|
||||||
|
},
|
||||||
|
stretch: {
|
||||||
|
default: any;
|
||||||
|
conditionalDifferencePercent: number // black bars less than this wide will trigger stretch
|
||||||
|
// if mode is set to '1'. 1.0=100%
|
||||||
|
},
|
||||||
|
kbm: { // TODO: we dont use this anymore
|
||||||
|
enabled: boolean, // if keyboard/mouse handler service will run
|
||||||
|
keyboardEnabled: boolean, // if keyboard shortcuts are processed
|
||||||
|
mouseEnabled: boolean, // if mouse movement is processed
|
||||||
|
}
|
||||||
|
|
||||||
zoom: {
|
zoom: {
|
||||||
minLogZoom: number,
|
minLogZoom: number,
|
||||||
maxLogZoom: number,
|
maxLogZoom: number,
|
||||||
announceDebounce: number // we wait this long before announcing new zoom
|
announceDebounce: number // we wait this long before announcing new zoom
|
||||||
},
|
},
|
||||||
|
|
||||||
miscSettings: {
|
miscSettings: {
|
||||||
mousePan: {
|
mousePan: {
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
@ -162,10 +372,7 @@ interface SettingsInterface {
|
|||||||
mousePanReverseMouse: boolean,
|
mousePanReverseMouse: boolean,
|
||||||
defaultAr?: any
|
defaultAr?: any
|
||||||
},
|
},
|
||||||
stretch: {
|
|
||||||
conditionalDifferencePercent: number // black bars less than this wide will trigger stretch
|
|
||||||
// if mode is set to '1'. 1.0=100%
|
|
||||||
},
|
|
||||||
resizer: {
|
resizer: {
|
||||||
setStyleString: {
|
setStyleString: {
|
||||||
maxRetries: number,
|
maxRetries: number,
|
||||||
@ -181,6 +388,7 @@ interface SettingsInterface {
|
|||||||
pan?: any,
|
pan?: any,
|
||||||
version?: string,
|
version?: string,
|
||||||
preventReload?: boolean,
|
preventReload?: boolean,
|
||||||
|
lastModified?: Date,
|
||||||
|
|
||||||
// -----------------------------------------
|
// -----------------------------------------
|
||||||
// ::: MITIGATIONS :::
|
// ::: MITIGATIONS :::
|
||||||
@ -193,101 +401,125 @@ interface SettingsInterface {
|
|||||||
limit?: number,
|
limit?: number,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// -----------------------------------------
|
// This object fulfills the same purpose as 'actions', but is written in less retarded and overly
|
||||||
// ::: ACTIONS :::
|
// complicated way. Hopefully it'll be easier to maintain it that way.
|
||||||
// -----------------------------------------
|
commands?: {
|
||||||
// Nastavitve za ukaze. Zamenja stare nastavitve za bližnične tipke.
|
crop?: CommandInterface[],
|
||||||
//
|
stretch?: CommandInterface[],
|
||||||
// Polje 'shortcut' je tabela, če se slučajno lotimo kdaj delati choordov.
|
zoom?: CommandInterface[],
|
||||||
actions: {
|
pan?: CommandInterface[],
|
||||||
name?: string, // name displayed in settings
|
internal?: CommandInterface[],
|
||||||
label?: string, // name displayed in ui (can be overridden in scope/playerUi)
|
},
|
||||||
cmd?: {
|
|
||||||
action: string,
|
|
||||||
arg: any,
|
|
||||||
customArg?: any,
|
|
||||||
persistent?: boolean, // optional, false by default. If true, change doesn't take effect immediately.
|
|
||||||
// Instead, this action saves stuff to settings
|
|
||||||
}[],
|
|
||||||
scopes?: {
|
|
||||||
global?: ActionScopeInterface,
|
|
||||||
site?: ActionScopeInterface,
|
|
||||||
page?: ActionScopeInterface
|
|
||||||
},
|
|
||||||
playerUi?: {
|
|
||||||
show: boolean,
|
|
||||||
path?: string,
|
|
||||||
},
|
|
||||||
userAdded?: boolean,
|
|
||||||
}[],
|
|
||||||
whatsNewChecked: boolean,
|
whatsNewChecked: boolean,
|
||||||
|
newFeatureTracker: any,
|
||||||
// -----------------------------------------
|
// -----------------------------------------
|
||||||
// ::: SITE CONFIGURATION :::
|
// ::: SITE CONFIGURATION :::
|
||||||
// -----------------------------------------
|
// -----------------------------------------
|
||||||
// Nastavitve za posamezno stran
|
|
||||||
// Config for a given page:
|
// Config for a given page:
|
||||||
//
|
//
|
||||||
// <hostname> : {
|
// <hostname> : {
|
||||||
// status: <option> // should extension work on this site?
|
// status: <option> // should extension work on this site?
|
||||||
// arStatus: <option> // should we do autodetection on this site?
|
// arStatus: <option> // should we do autodetection on this site?
|
||||||
//
|
//
|
||||||
// defaultAr?: <ratio> // automatically apply this aspect ratio on this side. Use extension defaults if undefined.
|
// defaultAr?: <ratio> // automatically apply this aspect ratio on this side. Use extension defaults if undefined.
|
||||||
// stretch? <stretch mode> // automatically stretch video on this site in this manner
|
// stretch? <stretch mode> // automatically stretch video on this site in this manner
|
||||||
// videoAlignment? <left|center|right>
|
// videoAlignment? <left|center|right>
|
||||||
//
|
//
|
||||||
// type: <official|community|user> // 'official' — blessed by Tam.
|
// type: <official|community|user> // 'official' — blessed by Tam.
|
||||||
// // 'community' — blessed by reddit.
|
// // 'community' — blessed by reddit.
|
||||||
// // 'user' — user-defined (not here)
|
// // 'user' — user-defined (not here)
|
||||||
// override: <true|false> // override user settings for this site on update
|
// override: <true|false> // override user settings for this site on update
|
||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
// Veljavne vrednosti za možnosti
|
|
||||||
// Valid values for options:
|
// Valid values for options:
|
||||||
//
|
//
|
||||||
// status, arStatus, statusEmbedded:
|
// status, arStatus, statusEmbedded:
|
||||||
//
|
//
|
||||||
// * enabled — always allow, full
|
// * enabled — always allow, full
|
||||||
// * basic — allow, but only the basic version without playerData
|
// * basic — allow, but only the basic version without playerData
|
||||||
// * default — allow if default is to allow, block if default is to block
|
// * default — allow if default is to allow, block if default is to block
|
||||||
// * disabled — never allow
|
// * disabled — never allow
|
||||||
//
|
//
|
||||||
sites: {
|
sites: {
|
||||||
[x: string]: {
|
[x: string]: SiteSettingsInterface,
|
||||||
mode?: ExtensionMode,
|
|
||||||
autoar?: ExtensionMode,
|
|
||||||
autoarFallback?: ExtensionMode,
|
|
||||||
stretch?: StretchType,
|
|
||||||
videoAlignment?: VideoAlignmentType,
|
|
||||||
keyboardShortcutsEnabled?: ExtensionMode,
|
|
||||||
type?: string,
|
|
||||||
override?: boolean,
|
|
||||||
arPersistence?: boolean,
|
|
||||||
actions?: any;
|
|
||||||
|
|
||||||
cropModePersistence?: CropModePersistence;
|
|
||||||
|
|
||||||
DOM?: {
|
|
||||||
player?: {
|
|
||||||
manual?: boolean,
|
|
||||||
querySelectors?: string,
|
|
||||||
additionalCss?: string,
|
|
||||||
useRelativeAncestor?: boolean,
|
|
||||||
videoAncestor?: any,
|
|
||||||
playerNodeCss?: string,
|
|
||||||
periodicallyRefreshPlayerElement?: boolean
|
|
||||||
},
|
|
||||||
video?: {
|
|
||||||
manual?: boolean,
|
|
||||||
querySelectors?: string,
|
|
||||||
additionalCss?: string,
|
|
||||||
useRelativeAncestor?: boolean,
|
|
||||||
playerNodeCss?: string
|
|
||||||
}
|
|
||||||
},
|
|
||||||
css?: string;
|
|
||||||
usePlayerArInFullscreen?: boolean;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SettingsInterface;
|
export interface SiteSettingsInterface {
|
||||||
|
notes?: string; // any special things related to this site.
|
||||||
|
|
||||||
|
enable: ExtensionMode;
|
||||||
|
enableAard: ExtensionMode;
|
||||||
|
enableKeyboard: InputHandlingMode;
|
||||||
|
enableUI: ExtensionMode;
|
||||||
|
|
||||||
|
applyToEmbeddedContent: EmbeddedContentSettingsOverridePolicy; // presumed to be 'Use as default' if not defined
|
||||||
|
overrideWhenEmbedded?: EmbeddedContentSettingsOverridePolicy;
|
||||||
|
|
||||||
|
|
||||||
|
autocreated?: boolean;
|
||||||
|
type?: SiteSupportLevel;
|
||||||
|
defaultType: SiteSupportLevel;
|
||||||
|
|
||||||
|
// must be defined in @global and @empty
|
||||||
|
persistCSA?: CropModePersistence, // CSA - crop, stretch, alignment
|
||||||
|
|
||||||
|
defaults?: { // must be defined in @global and @empty
|
||||||
|
crop?: {type: AspectRatioType, [x: string]: any},
|
||||||
|
stretch?: {type: StretchType, ratio?: number},
|
||||||
|
alignment?: {x: VideoAlignmentType, y: VideoAlignmentType},
|
||||||
|
}
|
||||||
|
|
||||||
|
cropModePersistence?: CropModePersistence;
|
||||||
|
stretchModePersistence?: CropModePersistence;
|
||||||
|
alignmentPersistence?: CropModePersistence;
|
||||||
|
|
||||||
|
playerAutoConfig?: PlayerAutoConfigInterface;
|
||||||
|
|
||||||
|
activeDOMConfig?: string;
|
||||||
|
DOMConfig?: { [x: string]: SiteDOMSettingsInterface };
|
||||||
|
|
||||||
|
// the following script are for extension caching and shouldn't be saved.
|
||||||
|
// if they _are_ saved, they will be overwritten
|
||||||
|
currentDOMConfig?: SiteDOMSettingsInterface;
|
||||||
|
|
||||||
|
// the following fields are for use with extension update script
|
||||||
|
override?: boolean; // whether settings for this site will be overwritten by extension upgrade script
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlayerAutoConfigInterface {
|
||||||
|
modified: boolean;
|
||||||
|
initialIndex: number;
|
||||||
|
currentIndex: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SiteDOMSettingsInterface {
|
||||||
|
type: SiteSupportLevel; // 'official' | 'community' | 'user-defined' | 'modified' | undefined;
|
||||||
|
elements?: {
|
||||||
|
player?: PlayerDOMSettingsInterface,
|
||||||
|
video?: PlayerDOMSettingsInterface,
|
||||||
|
};
|
||||||
|
customCss?: string;
|
||||||
|
periodicallyRefreshPlayerElement?: boolean;
|
||||||
|
|
||||||
|
// the following script are for extension caching and shouldn't be saved.
|
||||||
|
// if they _are_ saved, they will be overwritten
|
||||||
|
anchorElementIndex?: number;
|
||||||
|
anchorElement?: HTMLElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlayerDOMSettingsInterface {
|
||||||
|
detectionMode: PlayerDetectionMode,
|
||||||
|
allowAutoFallback: boolean,
|
||||||
|
ancestorIndex?: number,
|
||||||
|
querySelectors?: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SiteDOMElementSettingsInterface {
|
||||||
|
manual?: boolean;
|
||||||
|
querySelectors?: string;
|
||||||
|
index?: number; // previously: useRelativeAncestor + videoAncestor
|
||||||
|
mode?: 'index' | 'qs';
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SettingsInterface;
|
||||||
|
|||||||
7
src/common/interfaces/StretchInterface.ts
Normal file
7
src/common/interfaces/StretchInterface.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import StretchType from '../enums/StretchType.enum';
|
||||||
|
|
||||||
|
export interface Stretch {
|
||||||
|
type: StretchType,
|
||||||
|
ratio?: number,
|
||||||
|
limit?: number,
|
||||||
|
}
|
||||||
@ -2,16 +2,16 @@
|
|||||||
* For some reason, Chrome really doesn't like when chrome.runtime
|
* For some reason, Chrome really doesn't like when chrome.runtime
|
||||||
* methods are wrapped inside a ES6 proxy object. If 'port' is a
|
* methods are wrapped inside a ES6 proxy object. If 'port' is a
|
||||||
* ES6 Proxy of a Port object that `chrome.runtime.connect()` creates,
|
* ES6 Proxy of a Port object that `chrome.runtime.connect()` creates,
|
||||||
* then Chrome will do bullshits like `port.sendMessage` and
|
* then Chrome will do bullshits like `port.sendMessage` and
|
||||||
* `port.onMessage.addListener` crashing your Vue3 UI with bullshits
|
* `port.onMessage.addListener` crashing your Vue3 UI with bullshits
|
||||||
* excuses, e.g.
|
* excuses, e.g.
|
||||||
*
|
*
|
||||||
* | TypeError: Illegal invocation. Function must be called on
|
* | TypeError: Illegal invocation. Function must be called on
|
||||||
* | an object of type Port
|
* | an object of type Port
|
||||||
*
|
*
|
||||||
* which is some grade A bullshit because Firefox can handle that just
|
* which is some grade A bullshit because Firefox can handle that just
|
||||||
* fine.
|
* fine.
|
||||||
*
|
*
|
||||||
* There's two ways how I could handle this:
|
* There's two ways how I could handle this:
|
||||||
* * Find out how to get the original object from the proxy Vue3
|
* * Find out how to get the original object from the proxy Vue3
|
||||||
* creates, which would take time and ruin my xmass holiday, or
|
* creates, which would take time and ruin my xmass holiday, or
|
||||||
@ -19,8 +19,8 @@
|
|||||||
* the very real possibility that there's prolly a reason Chrome
|
* the very real possibility that there's prolly a reason Chrome
|
||||||
* does things in its own very special(tm) way, as if it had one
|
* does things in its own very special(tm) way, as if it had one
|
||||||
* extra chromosome over Firefox.
|
* extra chromosome over Firefox.
|
||||||
*
|
*
|
||||||
* Easy chhoice, really.
|
* Easy choice, really.
|
||||||
*/
|
*/
|
||||||
export class ChromeShittinessMitigations {
|
export class ChromeShittinessMitigations {
|
||||||
static port = null;
|
static port = null;
|
||||||
@ -30,4 +30,4 @@ export class ChromeShittinessMitigations {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ChromeShittinessMitigations;
|
export default ChromeShittinessMitigations;
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
class KeyboardShortcutParser {
|
class KeyboardShortcutParser {
|
||||||
static parseShortcut(keypress) {
|
static parseShortcut(keypress) {
|
||||||
let shortcutCombo = '';
|
let shortcutCombo = '';
|
||||||
|
|
||||||
if (keypress.ctrlKey) {
|
if (keypress.ctrlKey) {
|
||||||
shortcutCombo += 'Ctrl + ';
|
shortcutCombo += 'Ctrl + ';
|
||||||
}
|
}
|
||||||
@ -21,6 +21,20 @@ class KeyboardShortcutParser {
|
|||||||
}
|
}
|
||||||
return shortcutCombo;
|
return shortcutCombo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static generateShortcutFromKeypress(event) {
|
||||||
|
return {
|
||||||
|
ctrlKey: event.ctrlKey,
|
||||||
|
altKey: event.altKey,
|
||||||
|
shiftKey: event.shiftKey,
|
||||||
|
metaKey: event.metaKey,
|
||||||
|
code: event.code,
|
||||||
|
key: event.key,
|
||||||
|
keyup: true,
|
||||||
|
keydown: false,
|
||||||
|
type: event.type, // only needed for purposes of EditShortcutButton
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default KeyboardShortcutParser;
|
export default KeyboardShortcutParser;
|
||||||
|
|||||||
@ -1,3 +0,0 @@
|
|||||||
export async function sleep(timeout) {
|
|
||||||
return new Promise<void>( (resolve, reject) => setTimeout(() => resolve(), timeout));
|
|
||||||
}
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<p><i><a href="https://www.youtube.com/watch?v=Mn3YEJTSYs8&feature=youtu.be&t=770" target='_blank'>...</a> but as is normal, you can't use a free extensions without the developers begging for money. It's the bi-daily beggathon here on ... wherever here is.</i>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Jokes and references few will get aside, developing this extension does take a decent amount of time, motivation, carefully calibrated quantities
|
|
||||||
of alcohol and enough coffee to bankrupt a small nation.
|
|
||||||
</p>
|
|
||||||
<p>If you want to buy me a beer or bankroll my caffeine addiction, you can do so by <a href="https://paypal.me/tamius">clicking here</a>. All donations are appreciated.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
|
|
||||||
<script>
|
|
||||||
|
|
||||||
</script>
|
|
||||||
@ -1,37 +0,0 @@
|
|||||||
export default {
|
|
||||||
computed: {
|
|
||||||
scopeActions: function() {
|
|
||||||
return this.settings.active.actions.filter(x => {
|
|
||||||
if (! x.scopes) {
|
|
||||||
console.error('This action does not have a scope.', x);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return x.scopes[this.scope] && x.scopes[this.scope].show
|
|
||||||
}) || [];
|
|
||||||
},
|
|
||||||
extensionActions: function(){
|
|
||||||
return this.scopeActions.filter(x => x.cmd.length === 1 && x.cmd[0].action === 'set-ExtensionMode') || [];
|
|
||||||
},
|
|
||||||
aardActions: function(){
|
|
||||||
return this.scopeActions.filter(x => x.cmd.length === 1 && x.cmd[0].action === 'set-autoar-mode') || [];
|
|
||||||
},
|
|
||||||
aspectRatioActions: function(){
|
|
||||||
return this.scopeActions.filter(x => x.cmd.length === 1 && x.cmd[0].action === 'set-ar') || [];
|
|
||||||
},
|
|
||||||
cropModePersistenceActions: function() {
|
|
||||||
return this.scopeActions.filter(x => x.cmd.length === 1 && x.cmd[0].action === 'set-ar-persistence') || [];
|
|
||||||
},
|
|
||||||
stretchActions: function(){
|
|
||||||
return this.scopeActions.filter(x => x.cmd.length === 1 && x.cmd[0].action === 'set-stretch') || [];
|
|
||||||
},
|
|
||||||
keyboardActions: function() {
|
|
||||||
return this.scopeActions.filter(x => x.cmd.length === 1 && x.cmd[0].action === 'set-keyboard') || [];
|
|
||||||
},
|
|
||||||
alignmentActions: function() {
|
|
||||||
return this.scopeActions.filter(x => x.cmd.length === 1 && x.cmd[0].action === 'set-alignment') || [];
|
|
||||||
},
|
|
||||||
otherActions: function() {
|
|
||||||
return this.scopeActions.filter(x => x.cmd.length > 1) || [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,8 +1,8 @@
|
|||||||
import Debug from '../conf/Debug';
|
import Debug from '../../ext/conf/Debug';
|
||||||
|
|
||||||
class ObjectCopy {
|
class ObjectCopy {
|
||||||
static addNew(current, newValues){
|
static addNew(current, newValues){
|
||||||
|
|
||||||
// clone target
|
// clone target
|
||||||
let out = JSON.parse(JSON.stringify(newValues));
|
let out = JSON.parse(JSON.stringify(newValues));
|
||||||
|
|
||||||
@ -20,7 +20,7 @@ class ObjectCopy {
|
|||||||
|
|
||||||
// Types and constructors of objects must match. If they don't, we always use the new value.
|
// Types and constructors of objects must match. If they don't, we always use the new value.
|
||||||
if(typeof out[k] === typeof current[k] && out[k].constructor === current[k].constructor) {
|
if(typeof out[k] === typeof current[k] && out[k].constructor === current[k].constructor) {
|
||||||
|
|
||||||
// objects are special, we need to check them recursively.
|
// objects are special, we need to check them recursively.
|
||||||
if(out[k] && typeof out[k] === 'object' && out[k].constructor === Object ) {
|
if(out[k] && typeof out[k] === 'object' && out[k].constructor === Object ) {
|
||||||
if(Debug.debug && Debug.settings) {
|
if(Debug.debug && Debug.settings) {
|
||||||
@ -29,7 +29,7 @@ class ObjectCopy {
|
|||||||
|
|
||||||
out[k] = this.addNew(current[k], out[k]);
|
out[k] = this.addNew(current[k], out[k]);
|
||||||
} else {
|
} else {
|
||||||
out[k] = current[k];
|
out[k] = current[k];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -52,7 +52,7 @@ class ObjectCopy {
|
|||||||
if (current[k] !== undefined) {
|
if (current[k] !== undefined) {
|
||||||
// Types and constructors of objects must match. If they don't, we always use the new value.
|
// Types and constructors of objects must match. If they don't, we always use the new value.
|
||||||
if (typeof newValues[k] === typeof current[k] && newValues[k].constructor === current[k].constructor) {
|
if (typeof newValues[k] === typeof current[k] && newValues[k].constructor === current[k].constructor) {
|
||||||
|
|
||||||
// objects are special, we need to check them recursively.
|
// objects are special, we need to check them recursively.
|
||||||
if(current[k] && typeof current[k] === 'object' && current[k].constructor === Object ) {
|
if(current[k] && typeof current[k] === 'object' && current[k].constructor === Object ) {
|
||||||
if(Debug.debug && Debug.settings) {
|
if(Debug.debug && Debug.settings) {
|
||||||
@ -61,7 +61,7 @@ class ObjectCopy {
|
|||||||
|
|
||||||
current[k] = this.overwrite(current[k], newValues[k]);
|
current[k] = this.overwrite(current[k], newValues[k]);
|
||||||
} else {
|
} else {
|
||||||
current[k] = newValues[k];
|
current[k] = newValues[k];
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
current[k] = newValues[k];
|
current[k] = newValues[k];
|
||||||
@ -82,4 +82,4 @@ class ObjectCopy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ObjectCopy;
|
export default ObjectCopy;
|
||||||
14
src/common/utils/_cp.ts
Normal file
14
src/common/utils/_cp.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
/**
|
||||||
|
* Creates deep copy of an object
|
||||||
|
* @param obj
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function _cp(obj) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(JSON.stringify(obj));
|
||||||
|
} catch (e) {
|
||||||
|
// console.error('Failed to parse json. This probably means that the data we received was not an object. Will return data as-is');
|
||||||
|
// console.error('data in:', obj, 'error:', e);
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
}
|
||||||
12
src/common/utils/comparators.ts
Normal file
12
src/common/utils/comparators.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
export function collectionHas(collection, element): boolean {
|
||||||
|
for (let i = 0, len = collection.length; i < len; i++) {
|
||||||
|
if (collection[i] == element) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function equalish(a: number,b: number, tolerance: number): boolean {
|
||||||
|
return a > b - tolerance && a < b + tolerance;
|
||||||
|
}
|
||||||
103
src/common/utils/getElementStyles.ts
Normal file
103
src/common/utils/getElementStyles.ts
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
export type ProcessedElementStyles = {
|
||||||
|
[x: string]: {
|
||||||
|
css: string,
|
||||||
|
pxValue: number
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Note that this function is written _very_ dangerously
|
||||||
|
* and includes absolutely no error handling
|
||||||
|
*/
|
||||||
|
function getPixelValue(value: string, element?: HTMLElement, prop?: string) {
|
||||||
|
if (value === undefined || value === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.endsWith('px')) {
|
||||||
|
// console.log('value ends in px:', value)
|
||||||
|
return parseFloat(value);
|
||||||
|
}
|
||||||
|
if (value.endsWith('%')) {
|
||||||
|
// console.log('value ends in %:', value)
|
||||||
|
// This allegedly doesn't work for certain types of properties, allegedly.
|
||||||
|
const parent = element?.parentElement;
|
||||||
|
if (parent && prop) {
|
||||||
|
const parentDimensions = parent.getBoundingClientRect();
|
||||||
|
return (parseFloat(value) * 0.01) * (prop.includes('height') || ['top', 'bottom'].includes(prop) ? parentDimensions.height : parentDimensions.width);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.endsWith("vw")) {
|
||||||
|
const viewportWidth = window.innerWidth;
|
||||||
|
return (parseFloat(value) / 100) * viewportWidth;
|
||||||
|
}
|
||||||
|
if (value.endsWith("vh")) {
|
||||||
|
const viewportHeight = window.innerHeight;
|
||||||
|
return (parseFloat(value) / 100) * viewportHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.endsWith("rem")) {
|
||||||
|
const rootFontSize = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
||||||
|
return rootFontSize * parseFloat(value);
|
||||||
|
}
|
||||||
|
if (value.endsWith("em")) {
|
||||||
|
const fontSize = parseFloat(getComputedStyle(element).fontSize);
|
||||||
|
return fontSize * parseFloat(value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function getElementStyles(element: HTMLElement, props: string[], theoryProps?: string[]): ProcessedElementStyles {
|
||||||
|
const stylesheets = document.styleSheets;
|
||||||
|
const computedStyles = getComputedStyle(element);
|
||||||
|
const stylesOut = {};
|
||||||
|
|
||||||
|
for (const stylesheet of stylesheets) {
|
||||||
|
// console.log('——————————————— processing stylesheet:', stylesheet);
|
||||||
|
try {
|
||||||
|
for (const rule of stylesheet?.cssRules) {
|
||||||
|
if (rule instanceof CSSStyleRule) {
|
||||||
|
if (element.matches(rule.selectorText)) {
|
||||||
|
// console.log('element matches rule:', rule);
|
||||||
|
|
||||||
|
for (const property in rule.style) {
|
||||||
|
if (!props.includes(property)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cssValue = rule.style.getPropertyValue(property);
|
||||||
|
|
||||||
|
if (theoryProps?.includes(property)) {
|
||||||
|
stylesOut[property] = {css: cssValue};
|
||||||
|
}
|
||||||
|
|
||||||
|
const actualValue = computedStyles.getPropertyValue(property);
|
||||||
|
|
||||||
|
const theory = getPixelValue(cssValue, element, property);
|
||||||
|
const practice = getPixelValue(actualValue, element, property);
|
||||||
|
|
||||||
|
if (theory && practice && (Math.abs(theory - practice) < 1)) {
|
||||||
|
stylesOut[property] = {
|
||||||
|
css: cssValue,
|
||||||
|
pxValue: theory
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
// Cross-origin styles amy cause problems
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return stylesOut;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
49
src/common/utils/getHost.ts
Normal file
49
src/common/utils/getHost.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
|
||||||
|
/**
|
||||||
|
* Reqzests iframe parent hostname
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export async function getIframeParentHost(): Promise<string> {
|
||||||
|
return new Promise<any>((resolve) => {
|
||||||
|
let resolved = false;
|
||||||
|
let resendInterval;
|
||||||
|
|
||||||
|
function handleMessage(event) {
|
||||||
|
if (event.data.action === 'uw-parent-hostname') {
|
||||||
|
resolved = true;
|
||||||
|
clearInterval(resendInterval);
|
||||||
|
|
||||||
|
window.removeEventListener('message', handleMessage);
|
||||||
|
resolve(event.data.hostname);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('message', handleMessage);
|
||||||
|
resendInterval = setInterval(
|
||||||
|
() => {
|
||||||
|
if (!resolved) {
|
||||||
|
window.parent.postMessage(
|
||||||
|
{ action: 'uw-get-parent-hostname' },
|
||||||
|
'*'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
500
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function handleMessage(event) {
|
||||||
|
if (event.data.action === 'uw-get-parent-hostname') {
|
||||||
|
event.source.postMessage(
|
||||||
|
{action: 'uw-parent-hostname', hostname: window.location.hostname},
|
||||||
|
'*' as any
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setupHostnameReporting() {
|
||||||
|
window.removeEventListener('message', handleMessage); // setupHostnameReporting may run more than once
|
||||||
|
window.addEventListener('message', handleMessage);
|
||||||
|
}
|
||||||
@ -1,439 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div v-if="showLoggerUi" class="root-window flex flex-column overflow-hidden"
|
|
||||||
@keyup.stop
|
|
||||||
@keydown.stop
|
|
||||||
@keypress.stop
|
|
||||||
>
|
|
||||||
<div class="header">
|
|
||||||
<div class="header-top flex flex-row">
|
|
||||||
<div class="flex-grow">
|
|
||||||
<h1>{{header.header}}</h1>
|
|
||||||
</div>
|
|
||||||
<div class="button flex-noshrink button-header"
|
|
||||||
@click="hidePopup()"
|
|
||||||
>
|
|
||||||
<template v-if="logStringified">Finish logging</template>
|
|
||||||
<template v-else>Hide popup</template>
|
|
||||||
</div>
|
|
||||||
<!-- <div class="button flex-noshrink button-header"
|
|
||||||
@click="stopLogging()"
|
|
||||||
>
|
|
||||||
Stop logging
|
|
||||||
</div> -->
|
|
||||||
</div>
|
|
||||||
<div class="header-bottom">
|
|
||||||
<div>{{header.subheader}}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="content flex flex-row flex-grow overflow-hidden">
|
|
||||||
|
|
||||||
<!-- LOGGER SETTINGS PANEL -->
|
|
||||||
<div class="settings-panel flex flex-noshrink flex-column">
|
|
||||||
<div class="panel-top flex-nogrow">
|
|
||||||
<h2>Logger configuration</h2>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-row flex-end w100">
|
|
||||||
<div v-if="!showTextMode" class="button" @click="showTextMode = true">
|
|
||||||
<Icon icon="clipboard-plus" style="font-size: 2em"></Icon> Paste config ...
|
|
||||||
</div>
|
|
||||||
<div v-else class="button" @click="showTextMode = false">
|
|
||||||
Back
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="panel-middle scrollable flex-grow log-config-margin">
|
|
||||||
<template v-if="showTextMode">
|
|
||||||
<div ref="settingsEditArea"
|
|
||||||
style="white-space: pre-wrap; border: 1px solid orange; padding: 10px;"
|
|
||||||
class="monospace h100"
|
|
||||||
:class="{'jsonbg': !confHasError, 'jsonbg-error': confHasError}"
|
|
||||||
contenteditable="true"
|
|
||||||
@input="updateSettings"
|
|
||||||
>
|
|
||||||
{{parsedSettings}}
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<JsonObject label="logger-settings"
|
|
||||||
:value="currentSettings"
|
|
||||||
:ignoreKeys="{'allowLogging': true}"
|
|
||||||
@change="updateSettingsUi"
|
|
||||||
></JsonObject>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div class="flex flex-row flex-end">
|
|
||||||
<div class="button" @click="restoreLoggerSettings()">
|
|
||||||
Revert
|
|
||||||
</div>
|
|
||||||
<div class="button button-primary" @click="saveLoggerSettings()">
|
|
||||||
Save
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- LOGGER OUTPUT/START LOGGING -->
|
|
||||||
<div class="results-panel flex flex-shrink flex-column overflow-hidden">
|
|
||||||
<div class="panel-top flex-nogrow">
|
|
||||||
<h2>Logger results</h2>
|
|
||||||
</div>
|
|
||||||
<template v-if="logStringified">
|
|
||||||
<div v-if="confHasError" class="warn">
|
|
||||||
Logger configuration contains an error. You can export current log, but you will be unable to record a new log.
|
|
||||||
</div>
|
|
||||||
<div class="panel-middle scrollable flex-grow p-t-025em">
|
|
||||||
<pre>
|
|
||||||
{{logStringified}}
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
<div class="flex-noshrink flex flex-row flex-end p-t-025em">
|
|
||||||
<div class="button button-bar"
|
|
||||||
@click="startLogging()"
|
|
||||||
>
|
|
||||||
New log
|
|
||||||
</div>
|
|
||||||
<div class="button button-bar"
|
|
||||||
@click="exportLog()"
|
|
||||||
>
|
|
||||||
Export log
|
|
||||||
</div>
|
|
||||||
<div class="button button-bar button-primary"
|
|
||||||
@click="exportAndQuit()"
|
|
||||||
>
|
|
||||||
Export & finish
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<div class="panel-middle scrollable flex-grow">
|
|
||||||
<div v-if="!parsedSettings" class="text-center w100">
|
|
||||||
Please paste logger config into the text box to the left.
|
|
||||||
←←←
|
|
||||||
</div>
|
|
||||||
<div v-else-if="confHasError" class="warn">
|
|
||||||
Logger configuration contains an error. Cannot start logging.
|
|
||||||
</div>
|
|
||||||
<div v-else-if="lastSettings && lastSettings.allowLogging && lastSettings.consoleOptions && lastSettings.consoleOptions.enabled"
|
|
||||||
class="flex flex-column flex-center flex-cross-center w100 h100"
|
|
||||||
>
|
|
||||||
<p class="m-025em">
|
|
||||||
Logging in progress ...
|
|
||||||
</p>
|
|
||||||
<div class="button button-big button-primary"
|
|
||||||
@click="stopLogging()"
|
|
||||||
>
|
|
||||||
Stop logging
|
|
||||||
</div>
|
|
||||||
<template v-if="lastSettings && lastSettings.timeout"
|
|
||||||
class="m-025em"
|
|
||||||
>
|
|
||||||
<p>
|
|
||||||
... or wait until logging ends.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
You can <a @click="hidePopup()">hide popup</a> — it will automatically re-appear when logging finishes.
|
|
||||||
</p>
|
|
||||||
</template>
|
|
||||||
<template v-else-if="lastSettings">
|
|
||||||
<p>
|
|
||||||
You can <a @click="hidePopup()">hide popup</a> — the logging will continue until you re-open the popup and stop it.
|
|
||||||
</p>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
<div v-else class="flex flex-column flex-center flex-cross-center w100 h100">
|
|
||||||
<div class="button button-big button-primary"
|
|
||||||
@click="startLogging()"
|
|
||||||
>
|
|
||||||
Start logging
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- <div>
|
|
||||||
button row is heres
|
|
||||||
</div> -->
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { mapState } from 'vuex';
|
|
||||||
import Logger from '../ext/lib/Logger';
|
|
||||||
import Comms from '../ext/lib/comms/Comms';
|
|
||||||
import IO from '../common/js/IO';
|
|
||||||
import JsonObject from '../common/components/JsonEditor/JsonObject';
|
|
||||||
import Icon from '../common/components/Icon';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
JsonObject,
|
|
||||||
Icon,
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
showLoggerUi: false,
|
|
||||||
header: {
|
|
||||||
header: 'whoopsie daisy',
|
|
||||||
subheader: 'you broke the header choosing script'
|
|
||||||
},
|
|
||||||
parsedSettings: '',
|
|
||||||
lastSettings: {},
|
|
||||||
currentSettings: {},
|
|
||||||
confHasError: false,
|
|
||||||
logStringified: '',
|
|
||||||
showTextMode: false,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async created() {
|
|
||||||
const headerRotation = [{
|
|
||||||
header: "DEFORESTATOR 5000",
|
|
||||||
subheader: "Iron Legion's finest logging tool"
|
|
||||||
}, {
|
|
||||||
header: "Astinus",
|
|
||||||
subheader: "Ultrawidify logging tool"
|
|
||||||
}, {
|
|
||||||
header: "Tracer",
|
|
||||||
subheader: "I'm already printing stack traces"
|
|
||||||
}];
|
|
||||||
|
|
||||||
this.header = headerRotation[Math.floor(+Date.now() / (3600000*24)) % headerRotation.length] || this.header;
|
|
||||||
|
|
||||||
this.getLoggerSettings();
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
...mapState([
|
|
||||||
'uwLog',
|
|
||||||
'showLogger',
|
|
||||||
'loggingEnded',
|
|
||||||
]),
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
uwLog(newValue, oldValue) {
|
|
||||||
if (oldValue !== newValue) {
|
|
||||||
this.$store.dispatch('uw-show-logger');
|
|
||||||
this.logStringified = JSON.stringify(newValue, null, 2);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async showLogger(newValue) {
|
|
||||||
this.showLoggerUi = newValue;
|
|
||||||
|
|
||||||
// update logger settings (they could have changed while the popup was closed)
|
|
||||||
if (newValue) {
|
|
||||||
this.getLoggerSettings();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
loggingEnded(newValue) {
|
|
||||||
// note — the value of loggingEnded never actually matters. Even if this value is 'true'
|
|
||||||
// internally, vuexStore.dspatch() will still do its job and give us the signal we want
|
|
||||||
if (newValue) {
|
|
||||||
this.stopLogging();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
async getLoggerSettings() {
|
|
||||||
this.lastSettings = await Logger.getConfig() || {};
|
|
||||||
this.parsedSettings = JSON.stringify(this.lastSettings, null, 2) || '';
|
|
||||||
this.currentSettings = JSON.parse(JSON.stringify(this.lastSettings));
|
|
||||||
},
|
|
||||||
updateSettings(val) {
|
|
||||||
try {
|
|
||||||
this.parsedSettings = JSON.stringify(JSON.parse(val.target.textContent.trim()), null, 2);
|
|
||||||
this.lastSettings = JSON.parse(val.target.textContent.trim());
|
|
||||||
this.currentSettings = JSON.parse(JSON.stringify(this.lastSettings));
|
|
||||||
this.confHasError = false;
|
|
||||||
} catch (e) {
|
|
||||||
this.confHasError = true;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
updateSettingsUi(val) {
|
|
||||||
try {
|
|
||||||
this.parsedSettings = JSON.stringify(val, null, 2);
|
|
||||||
this.lastSettings = val;
|
|
||||||
this.currentSettings = JSON.parse(JSON.stringify(this.lastSettings));
|
|
||||||
} catch (e) {
|
|
||||||
|
|
||||||
}
|
|
||||||
},
|
|
||||||
saveLoggerSettings() {
|
|
||||||
Logger.saveConfig({...this.lastSettings});
|
|
||||||
},
|
|
||||||
restoreLoggerSettings() {
|
|
||||||
this.getLoggerSettings();
|
|
||||||
this.confHasError = false;
|
|
||||||
},
|
|
||||||
async startLogging(){
|
|
||||||
this.logStringified = undefined;
|
|
||||||
await Logger.saveConfig({...this.lastSettings, allowLogging: true});
|
|
||||||
window.location.reload();
|
|
||||||
},
|
|
||||||
hidePopup() {
|
|
||||||
// this function only works as 'close' if logging has finished
|
|
||||||
if (this.logStringified) {
|
|
||||||
Logger.saveConfig({...this.lastSettings, allowLogging: false});
|
|
||||||
this.logStringified = undefined;
|
|
||||||
}
|
|
||||||
this.$store.dispatch('uw-hide-logger');
|
|
||||||
},
|
|
||||||
closePopupAndStopLogging() {
|
|
||||||
Logger.saveConfig({...this.lastSettings, allowLogging: false});
|
|
||||||
this.logStringified = undefined;
|
|
||||||
this.$store.dispatch('uw-hide-logger');
|
|
||||||
},
|
|
||||||
stopLogging() {
|
|
||||||
Logger.saveConfig({...this.lastSettings, allowLogging: false});
|
|
||||||
this.lastSettings.allowLogging = false;
|
|
||||||
},
|
|
||||||
exportLog() {
|
|
||||||
IO.csStringToFile(this.logStringified);
|
|
||||||
},
|
|
||||||
exportAndQuit() {
|
|
||||||
this.exportLog();
|
|
||||||
this.logStringified = undefined;
|
|
||||||
this.closePopupAndStopLogging();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" src="../res/css/flex.scss" scoped></style>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import '../res/css/colors.scss';
|
|
||||||
@import '../res/css/font/overpass.css';
|
|
||||||
@import '../res/css/font/overpass-mono.css';
|
|
||||||
@import '../res/css/common.scss';
|
|
||||||
|
|
||||||
.uw-ultrawidify-container-root {
|
|
||||||
.root-window {
|
|
||||||
position: fixed !important;
|
|
||||||
top: 5vh !important;
|
|
||||||
left: 5vw !important;
|
|
||||||
width: 90vw !important;
|
|
||||||
height: 90vh !important;
|
|
||||||
z-index: 999999 !important;
|
|
||||||
background-color: rgba( $page-background, 0.9) !important;
|
|
||||||
color: #f1f1f1 !important;
|
|
||||||
font-size: 14px !important;
|
|
||||||
|
|
||||||
box-sizing: border-box !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
div {
|
|
||||||
font-family: 'Overpass';
|
|
||||||
}
|
|
||||||
|
|
||||||
h1, h2 {
|
|
||||||
font-family: 'Overpass Thin';
|
|
||||||
}
|
|
||||||
h1 {
|
|
||||||
font-size: 4em;
|
|
||||||
}
|
|
||||||
h2 {
|
|
||||||
font-size: 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
h1 {
|
|
||||||
margin-bottom: -0.20em;
|
|
||||||
margin-top: 0.0em;
|
|
||||||
}
|
|
||||||
.header-top, .header-bottom {
|
|
||||||
padding-left: 16px;
|
|
||||||
padding-right: 16px;
|
|
||||||
}
|
|
||||||
.header-top {
|
|
||||||
background-color: $popup-header-background !important;
|
|
||||||
}
|
|
||||||
.header-bottom {
|
|
||||||
font-size: 1.75em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.content {
|
|
||||||
box-sizing: border-box;
|
|
||||||
padding: 8px 32px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.settings-panel {
|
|
||||||
box-sizing: border-box;
|
|
||||||
padding-right: 8px;
|
|
||||||
flex-grow: 2 !important;
|
|
||||||
min-width: 30% !important;
|
|
||||||
flex-shrink: 0 !important;
|
|
||||||
height: inherit !important;
|
|
||||||
}
|
|
||||||
.results-panel {
|
|
||||||
box-sizing: border-box;
|
|
||||||
padding-left: 8px;
|
|
||||||
max-width: 70% !important;
|
|
||||||
flex-grow: 5 !important;
|
|
||||||
flex-shrink: 0 !important;
|
|
||||||
height: inherit !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scrollable {
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.overflow-hidden {
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
pre {
|
|
||||||
font-family: 'Overpass Mono';
|
|
||||||
}
|
|
||||||
|
|
||||||
.m-025em {
|
|
||||||
margin: 0.25em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.p-t-025em {
|
|
||||||
padding-top: 0.25em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-items: center;
|
|
||||||
padding-left: 2em;
|
|
||||||
padding-right: 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-primary {
|
|
||||||
background-color: $primary;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-big {
|
|
||||||
font-size: 1.5em;
|
|
||||||
padding: 1.75em 3.25em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-bar {
|
|
||||||
font-size: 1.25em;
|
|
||||||
padding: 0.25em 1.25em;
|
|
||||||
margin-left: 0.25em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-header {
|
|
||||||
font-size: 2em;
|
|
||||||
padding-top: 0.1em;
|
|
||||||
padding-left: 1em;
|
|
||||||
padding-right: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.jsonbg {
|
|
||||||
background-color: #131313;
|
|
||||||
}
|
|
||||||
.jsonbg-error {
|
|
||||||
background-color: #884420;
|
|
||||||
}
|
|
||||||
|
|
||||||
.log-config-margin {
|
|
||||||
margin-top: 3em;
|
|
||||||
margin-bottom: 3em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,216 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div v-if="showNotification" class="uw-ultrawidify-container flex flex-column overflow-hidden">
|
|
||||||
<div class="notification-popup flex flex-row">
|
|
||||||
<div v-if="notificationIcon" class="flex-nogrow flex-noshrink notification-icon">
|
|
||||||
<Icon
|
|
||||||
class="flex-nogrow flex-noshrink"
|
|
||||||
:icon="notificationIcon"
|
|
||||||
>
|
|
||||||
</Icon>
|
|
||||||
</div>
|
|
||||||
<div class="notification-content flex-grow flex-shrink flex flex-column flex-cross-center">
|
|
||||||
<div
|
|
||||||
class="notification-text"
|
|
||||||
v-html="notificationText"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="notificationActions"
|
|
||||||
class="action-buttons flex flex-row"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-for="action of notificationActions"
|
|
||||||
class="action-button"
|
|
||||||
:key="action"
|
|
||||||
@click="action.command"
|
|
||||||
>
|
|
||||||
<Icon v-if="action.icon" :icon="action.icon"></Icon>{{action.label}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="hideActions"
|
|
||||||
class="hide-actions"
|
|
||||||
>
|
|
||||||
Never show again:<wbr>
|
|
||||||
<template
|
|
||||||
v-for="action of hideActions"
|
|
||||||
:key="action"
|
|
||||||
>
|
|
||||||
<i @click="closeNotification">
|
|
||||||
<a
|
|
||||||
class="hide-action-button"
|
|
||||||
@click="action.command"
|
|
||||||
>
|
|
||||||
{{action.label}}
|
|
||||||
</a>
|
|
||||||
<wbr>
|
|
||||||
</i>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="notification-icon action-button"
|
|
||||||
@click="closeNotification()"
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
class="flex-nogrow flex-noshrink"
|
|
||||||
icon="x"
|
|
||||||
>
|
|
||||||
</Icon>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { mapState } from 'vuex';
|
|
||||||
import Icon from '../common/components/Icon';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
Icon,
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
notificationTimeout: null,
|
|
||||||
notificationIcon: "exclamation-triangle",
|
|
||||||
notificationText: "<b>Sample text.</b> This will be replaced with real notification later.",
|
|
||||||
notificationActions: null,
|
|
||||||
hideActions: null,
|
|
||||||
showNotification: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
...mapState([
|
|
||||||
'notificationConfig'
|
|
||||||
]),
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
/**
|
|
||||||
* Sets new notification config. Currently, we can only show one notification at a time.
|
|
||||||
*
|
|
||||||
* We expect a config object like this:
|
|
||||||
* {
|
|
||||||
* timeout: number — how long we'll be displaying the notification. If empty, 10s. -1: until user dismisses it
|
|
||||||
* icon: string — what icon we're gonna show. We're using bootstrap icons. Can be empty.
|
|
||||||
* text: — notification text. Supports HTML.
|
|
||||||
* notificationActions: [
|
|
||||||
* {
|
|
||||||
* command: function that gets executed upon clicking the button.
|
|
||||||
* label: label of the button
|
|
||||||
* icon: icon of the button
|
|
||||||
* }
|
|
||||||
* ],
|
|
||||||
* hideOptions: [
|
|
||||||
* // more of notificationActions except it's a special case
|
|
||||||
* ]
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
notificationConfig(newConfig) {
|
|
||||||
if (newConfig) {
|
|
||||||
this.notificationText = newConfig.text;
|
|
||||||
this.notificationActions = newConfig.notificationActions;
|
|
||||||
this.notificationIcon = newConfig.icon;
|
|
||||||
this.hideActions = newConfig.hideActions;
|
|
||||||
|
|
||||||
this.showNotification = true;
|
|
||||||
|
|
||||||
if (newConfig.timeout !== -1) {
|
|
||||||
this.notificationTimeout = setTimeout(() => this.closeNotification(), newConfig.timeout ?? 5000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
closeNotification() {
|
|
||||||
clearTimeout(this.notificationTimeout);
|
|
||||||
|
|
||||||
this.showNotification = false;
|
|
||||||
this.notificationIcon = null;
|
|
||||||
this.notificationText = null;
|
|
||||||
this.notificationActions = null;
|
|
||||||
this.hideActions = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import '../res/css/uwui-base.scss';
|
|
||||||
@import '../res/css/colors.scss';
|
|
||||||
@import '../res/css/font/overpass.css';
|
|
||||||
@import '../res/css/font/overpass-mono.css';
|
|
||||||
@import '../res/css/common.scss';
|
|
||||||
|
|
||||||
.uw-ultrawidify-container-root {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
pointer-events: none;
|
|
||||||
|
|
||||||
display: block !important;
|
|
||||||
position: relative !important;
|
|
||||||
width: 100% !important;
|
|
||||||
height: 100% !important;
|
|
||||||
pointer-events: none !important;
|
|
||||||
|
|
||||||
font-size: 16px !important;
|
|
||||||
|
|
||||||
.notification-popup {
|
|
||||||
pointer-events: auto !important;
|
|
||||||
position: absolute;
|
|
||||||
z-index: 99999999;
|
|
||||||
top: 2em;
|
|
||||||
right: 2em;
|
|
||||||
width: 32em;
|
|
||||||
|
|
||||||
padding: 0.7em 0.5em;
|
|
||||||
|
|
||||||
font-family: 'Overpass';
|
|
||||||
|
|
||||||
background-color: rgba(108, 55, 12, 0.779);
|
|
||||||
color: #fff;
|
|
||||||
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notifcation-content {
|
|
||||||
margin-left: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification-text {
|
|
||||||
text-align: justify;
|
|
||||||
padding-left: 0.5em;
|
|
||||||
padding-right: 0.25em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification-icon {
|
|
||||||
font-size: 3em;
|
|
||||||
line-height: 0.5;
|
|
||||||
}
|
|
||||||
.action-button {
|
|
||||||
pointer-events: auto;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hide-actions {
|
|
||||||
color: #ccc;
|
|
||||||
font-size: 0.8em;
|
|
||||||
justify-self: flex-end;
|
|
||||||
align-self: flex-end;
|
|
||||||
margin-top: 1em;
|
|
||||||
margin-bottom: -1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hide-action-button {
|
|
||||||
color: #eee;
|
|
||||||
font-size: 0.9em;
|
|
||||||
text-decoration: underline;
|
|
||||||
text-decoration-color: rgba(255,255,255,0.5);
|
|
||||||
|
|
||||||
pointer-events: auto;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,177 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="uw-hover uv-hover-trigger-region">
|
|
||||||
TEST CONTENT
|
|
||||||
</div>
|
|
||||||
<div class="popup-panel">
|
|
||||||
<div class="tab-row flex flex-row">
|
|
||||||
<div class="tab">
|
|
||||||
todo: icon<br/>
|
|
||||||
Video options
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>sudpo
|
|
||||||
<!-- Panel section -->
|
|
||||||
<template v-if="settingsInitialized">
|
|
||||||
<VideoSettings
|
|
||||||
:settings="settings"
|
|
||||||
></VideoSettings>
|
|
||||||
<ResizerDebugPanel :debugData="debugData">
|
|
||||||
</ResizerDebugPanel>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import VideoSettings from './PlayerUiPanels/VideoSettings.vue'
|
|
||||||
import { mapState } from 'vuex';
|
|
||||||
import Icon from '../common/components/Icon';
|
|
||||||
import ResizerDebugPanel from './PlayerUiPanels/ResizerDebugPanelComponent';
|
|
||||||
import BrowserDetect from '../ext/conf/BrowserDetect';
|
|
||||||
import ExecAction from './ui-libs/ExecAction';
|
|
||||||
import Logger from '../ext/lib/Logger';
|
|
||||||
import Settings from '../ext/lib/Settings';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
Icon,
|
|
||||||
ResizerDebugPanel, VideoSettings
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
// component properties
|
|
||||||
settings: {},
|
|
||||||
settingsInitialized: false,
|
|
||||||
execAction: new ExecAction(),
|
|
||||||
logger: null,
|
|
||||||
|
|
||||||
uiVisible: true,
|
|
||||||
debugData: {
|
|
||||||
resizer: {},
|
|
||||||
player: {},
|
|
||||||
},
|
|
||||||
debugDataPrettified: ''
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
...mapState([
|
|
||||||
'showUi',
|
|
||||||
'resizerDebugData',
|
|
||||||
'playerDebugData'
|
|
||||||
]),
|
|
||||||
windowWidth: () => {
|
|
||||||
return window.innerWidth;
|
|
||||||
},
|
|
||||||
windowHeight: () => {
|
|
||||||
return window.innerHeight;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
showUi(visible) {
|
|
||||||
if (visible !== undefined) {
|
|
||||||
this.uiVisible = visible;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
resizerDebugData(newData) {
|
|
||||||
this.debugData.resizer = newData;
|
|
||||||
this.debugDataPrettified = JSON.stringify(this.debugData, null, 2);
|
|
||||||
},
|
|
||||||
playerDebugData(newData) {
|
|
||||||
this.debugData.player = newData;
|
|
||||||
this.debugDataPrettified = JSON.stringify(this.debugData, null, 2);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async created() {
|
|
||||||
try {
|
|
||||||
this.logger = new Logger();
|
|
||||||
await this.logger.init({
|
|
||||||
allowLogging: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.settings = new Settings({afterSettingsSaved: this.updateConfig, logger: this.logger});
|
|
||||||
await this.settings.init();
|
|
||||||
this.settingsInitialized = true;
|
|
||||||
|
|
||||||
this.execAction.setSettings(this.settings);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to initiate ultrawidify player ui.', e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
getUrl(url) {
|
|
||||||
return BrowserDetect.getURL(url);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" src="../res/css/uwui-base.scss" scoped></style>
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import '../res/css/uwui-base.scss';
|
|
||||||
@import '../res/css/colors.scss';
|
|
||||||
@import '../res/css/font/overpass.css';
|
|
||||||
@import '../res/css/font/overpass-mono.css';
|
|
||||||
@import '../res/css/common.scss';
|
|
||||||
|
|
||||||
.uw-ultrawidify-container-root {
|
|
||||||
// .relative-wrapper {
|
|
||||||
// position: relative;
|
|
||||||
// width: 100%;
|
|
||||||
// height: 100%;
|
|
||||||
// }
|
|
||||||
|
|
||||||
.uw-hover {
|
|
||||||
position: absolute;
|
|
||||||
top: 10%;
|
|
||||||
left: 10%;
|
|
||||||
width: 100px;
|
|
||||||
height: 100px;
|
|
||||||
color: #fff;
|
|
||||||
background-color: #000;
|
|
||||||
|
|
||||||
z-index: 999999999999999999;
|
|
||||||
}
|
|
||||||
.uw-hover:hover {
|
|
||||||
background-color: #f00;
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup-panel {
|
|
||||||
position: absolute;
|
|
||||||
|
|
||||||
top: 10%;
|
|
||||||
left: 10%;
|
|
||||||
|
|
||||||
z-index: 999999999999999999;
|
|
||||||
|
|
||||||
width: 2500px;
|
|
||||||
height: 1200px;
|
|
||||||
max-width: 80%;
|
|
||||||
max-height: 80%;
|
|
||||||
|
|
||||||
pointer-events: all !important;
|
|
||||||
|
|
||||||
background-color: rgba(0,0,0,0.69);
|
|
||||||
color: #fff;
|
|
||||||
|
|
||||||
overflow-y: auto;
|
|
||||||
|
|
||||||
.tab {
|
|
||||||
display: block;
|
|
||||||
height: 42px;
|
|
||||||
font-size: 2.5rem;
|
|
||||||
background: rgb(87, 54, 26);
|
|
||||||
}
|
|
||||||
.tab:hover {
|
|
||||||
background-color: #f00;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pre {
|
|
||||||
white-space: pre-wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
</style>
|
|
||||||
@ -1,55 +1,42 @@
|
|||||||
import Debug from './conf/Debug';
|
import Debug from '@src/ext/conf/Debug';
|
||||||
import ExtensionMode from '../common/enums/ExtensionMode.enum';
|
import CommsClient, { CommsOrigin } from '@src/ext/module/comms/CommsClient';
|
||||||
import Settings from './lib/Settings';
|
import EventBus from '@src/ext/module/EventBus';
|
||||||
import ActionHandler from './lib/ActionHandler';
|
import KeyboardHandler from '@src/ext/module/kbm/KeyboardHandler';
|
||||||
import Comms from './lib/comms/Comms';
|
import { ComponentLogger } from '@src/ext/module/logging/ComponentLogger';
|
||||||
import CommsClient from './lib/comms/CommsClient';
|
import { BLANK_LOGGER_CONFIG, LogAggregator } from '@src/ext/module/logging/LogAggregator';
|
||||||
import PageInfo from './lib/video-data/PageInfo';
|
import Settings from '@src/ext/module/settings/Settings';
|
||||||
import Logger, { baseLoggingOptions } from './lib/Logger';
|
import { SiteSettings } from '@src/ext/module/settings/SiteSettings';
|
||||||
|
import UI from '@src/ext/module/uwui/UI';
|
||||||
|
import PageInfo from '@src/ext/module/video-data/PageInfo';
|
||||||
|
import { getIframeParentHost, setupHostnameReporting } from '@src/common/utils/getHost';
|
||||||
|
|
||||||
export default class UWContent {
|
export default class UWContent {
|
||||||
pageInfo: PageInfo;
|
pageInfo: PageInfo;
|
||||||
comms: CommsClient;
|
comms: CommsClient;
|
||||||
settings: Settings;
|
settings: Settings;
|
||||||
actionHandler: ActionHandler;
|
siteSettings: SiteSettings;
|
||||||
logger: Logger;
|
keyboardHandler: KeyboardHandler;
|
||||||
|
logAggregator: LogAggregator;
|
||||||
|
logger: ComponentLogger;
|
||||||
|
eventBus: EventBus;
|
||||||
|
isIframe: boolean = false;
|
||||||
|
parentHostname: string;
|
||||||
|
|
||||||
|
globalUi: any;
|
||||||
|
|
||||||
commsHandlers: {
|
commsHandlers: {
|
||||||
[x: string]: ((a: any, b?: any) => void | Promise<void>)[]
|
[x: string]: ((a: any, b?: any) => void | Promise<void>)[]
|
||||||
} = {
|
} = {
|
||||||
'get-current-zoom': [() => this.pageInfo.requestCurrentZoom()],
|
|
||||||
'set-ar': [(message) => this.pageInfo.setAr({type: message.arg, ratio: message.customArg}, message.playing)],
|
|
||||||
'set-alignment': [(message) => {
|
|
||||||
this.pageInfo.setVideoAlignment(message.arg, message.playing);
|
|
||||||
this.pageInfo.restoreAr();
|
|
||||||
}],
|
|
||||||
'set-stretch': [(message) => this.pageInfo.setStretchMode(message.arg, message.playing, message.customArg)],
|
|
||||||
'set-keyboard': [(message) => this.pageInfo.setKeyboardShortcutsEnabled(message.arg)],
|
|
||||||
'autoar-start': [(message) => {
|
|
||||||
if (message.enabled !== false) {
|
|
||||||
this.pageInfo.initArDetection(message.playing);
|
|
||||||
this.pageInfo.startArDetection(message.playing);
|
|
||||||
} else {
|
|
||||||
this.pageInfo.stopArDetection(message.playing);
|
|
||||||
}
|
|
||||||
}],
|
|
||||||
'pause-processing': [(message) => this.pageInfo.pauseProcessing(message.playing)],
|
|
||||||
'resume-processing': [(message) => this.pageInfo.resumeProcessing(message.autoArStatus, message.playing)],
|
|
||||||
'set-zoom': [(message) => this.pageInfo.setZoom(message.arg, true, message.playing)],
|
|
||||||
'change-zoom': [(message) => this.pageInfo.zoomStep(message.arg, message.playing)],
|
|
||||||
'mark-player': [(message) => this.pageInfo.markPlayer(message.name, message.color)],
|
|
||||||
'unmark-player': [() => this.pageInfo.unmarkPlayer()],
|
|
||||||
'autoar-set-manual-tick': [(message) => this.pageInfo.setManualTick(message.arg)],
|
|
||||||
'autoar-tick': [() => this.pageInfo.tick()],
|
|
||||||
'set-ar-persistence': [(message) => this.pageInfo.setArPersistence(message.arg)],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(){
|
constructor(){
|
||||||
|
setupHostnameReporting();
|
||||||
|
this.isIframe = window.self !== window.top;
|
||||||
}
|
}
|
||||||
|
|
||||||
reloadSettings() {
|
reloadSettings() {
|
||||||
try {
|
try {
|
||||||
this.logger.log('info', 'debug', 'Things happened in the popup. Will reload extension settings.');
|
this.logger.debug('reloadSettings', 'Things happened in the popup. Will reload extension settings.');
|
||||||
this.init();
|
this.init();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('Ultrawidify: settings reload failed. This probably shouldn\'t outright kill the extension, but page reload is recommended.');
|
console.warn('Ultrawidify: settings reload failed. This probably shouldn\'t outright kill the extension, but page reload is recommended.');
|
||||||
@ -57,87 +44,102 @@ export default class UWContent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async init(){
|
async init(){
|
||||||
|
if (this.isIframe) {
|
||||||
|
this.parentHostname = await getIframeParentHost();
|
||||||
|
console.warn('[uw-content] got iframe parent:', this.parentHostname);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (Debug.debug) {
|
if (Debug.debug) {
|
||||||
console.log("[uw::main] loading configuration ...");
|
console.log("[uw::main] loading configuration ...");
|
||||||
}
|
}
|
||||||
|
|
||||||
// logger init is the first thing that needs to run
|
// logger init is the first thing that needs to run
|
||||||
try {
|
try {
|
||||||
if (!this.logger) {
|
if (!this.logger) {
|
||||||
|
this.logAggregator = new LogAggregator('◈');
|
||||||
this.logger = new Logger();
|
this.logger = new ComponentLogger(this.logAggregator, 'UWContent');
|
||||||
await this.logger.init(baseLoggingOptions);
|
await this.logAggregator.init(BLANK_LOGGER_CONFIG);
|
||||||
|
|
||||||
// show popup if logging to file is enabled
|
|
||||||
if (this.logger.isLoggingAllowed() && this.logger.isLoggingToFile()) {
|
|
||||||
console.info("[uw::init] Logging is allowed! Initalizing vue and UI!");
|
|
||||||
|
|
||||||
// CommsClient is not initiated yet, so we use static comms to send the command
|
|
||||||
Comms.sendMessage({cmd: 'show-logger'});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("logger init failed!", e)
|
console.error("logger init failed!", e)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// init() is re-run any time settings change
|
// init() is re-run any time settings change
|
||||||
if (this.comms) {
|
if (this.comms) {
|
||||||
this.comms.destroy();
|
this.comms.destroy();
|
||||||
}
|
}
|
||||||
|
if (this.eventBus) {
|
||||||
|
this.eventBus.destroy();
|
||||||
|
}
|
||||||
if (!this.settings) {
|
if (!this.settings) {
|
||||||
this.settings = new Settings({
|
this.settings = new Settings({
|
||||||
onSettingsChanged: () => this.reloadSettings(),
|
onSettingsChanged: () => this.reloadSettings(),
|
||||||
logger: this.logger
|
logAggregator: this.logAggregator
|
||||||
});
|
});
|
||||||
await this.settings.init();
|
await this.settings.init();
|
||||||
|
this.siteSettings = this.settings.getSiteSettings({site: window.location.hostname, isIframe: this.isIframe, parentHostname: this.parentHostname});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.comms = new CommsClient('content-main-port', this.logger, this.commsHandlers);
|
|
||||||
|
|
||||||
// če smo razširitev onemogočili v nastavitvah, ne naredimo ničesar
|
this.eventBus = new EventBus({name: 'content-script', commsOrigin: CommsOrigin.ContentScript});
|
||||||
// If extension is soft-disabled, don't do shit
|
this.eventBus.subscribe(
|
||||||
|
'uw-restart',
|
||||||
var extensionMode = this.settings.getExtensionMode();
|
{
|
||||||
|
source: this,
|
||||||
this.logger.log('info', 'debug', "[uw::init] Extension mode:" + (extensionMode < 0 ? "disabled" : extensionMode == '1' ? 'basic' : 'full'));
|
function: () => this.initPhase2()
|
||||||
|
|
||||||
const isSiteDisabled = extensionMode === ExtensionMode.Disabled
|
|
||||||
|
|
||||||
if (isSiteDisabled) {
|
|
||||||
if (this.settings.getExtensionMode('@global') === ExtensionMode.Disabled) {
|
|
||||||
this.logger.log('info', 'debug', "[uw::init] EXTENSION DISABLED, THEREFORE WONT BE STARTED")
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
);
|
||||||
|
this.comms = new CommsClient('content-main-port', this.logAggregator, this.eventBus);
|
||||||
try {
|
this.eventBus.setComms(this.comms);
|
||||||
if (this.pageInfo) {
|
|
||||||
this.logger.log('info', 'debug', '[uw.js::setup] An instance of pageInfo already exists and will be destroyed.');
|
|
||||||
this.pageInfo.destroy();
|
|
||||||
}
|
|
||||||
this.pageInfo = new PageInfo(this.comms, this.settings, this.logger, extensionMode, isSiteDisabled);
|
|
||||||
this.logger.log('info', 'debug', "[uw.js::setup] pageInfo initialized.");
|
|
||||||
|
|
||||||
this.logger.log('info', 'debug', "[uw.js::setup] will try to initate ActionHandler.");
|
|
||||||
|
|
||||||
// start action handler only if extension is enabled for this site
|
|
||||||
if (!isSiteDisabled) {
|
|
||||||
if (this.actionHandler) {
|
|
||||||
this.actionHandler.destroy();
|
|
||||||
}
|
|
||||||
this.actionHandler = new ActionHandler(this.pageInfo);
|
|
||||||
this.actionHandler.init();
|
|
||||||
|
|
||||||
this.logger.log('info', 'debug', "[uw.js::setup] ActionHandler initiated.");
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (e) {
|
this.initPhase2();
|
||||||
console.error('Ultrawidify: failed to start extension. Error:', e)
|
|
||||||
this.logger.log('error', 'debug', "[uw::init] FAILED TO START EXTENSION. Error:", e);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Ultrawidify initalization failed for some reason:', e);
|
console.error('Ultrawidify initialization failed for some reason:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// we always initialize extension, even if it's disabled.
|
||||||
|
initPhase2() {
|
||||||
|
try {
|
||||||
|
if (this.pageInfo) {
|
||||||
|
this.logger.info('setup', 'An instance of pageInfo already exists and will be destroyed.');
|
||||||
|
this.pageInfo.destroy();
|
||||||
|
}
|
||||||
|
this.pageInfo = new PageInfo(this.eventBus, this.siteSettings, this.settings, this.logAggregator);
|
||||||
|
this.logger.debug('setup', "pageInfo initialized.");
|
||||||
|
|
||||||
|
this.logger.debug('setup', "will try to initiate KeyboardHandler.");
|
||||||
|
|
||||||
|
if (this.keyboardHandler) {
|
||||||
|
this.keyboardHandler.destroy();
|
||||||
|
}
|
||||||
|
this.keyboardHandler = new KeyboardHandler(this.eventBus, this.siteSettings, this.settings, this.logAggregator);
|
||||||
|
this.keyboardHandler.init();
|
||||||
|
|
||||||
|
this.logger.debug('setup', "KeyboardHandler initiated.");
|
||||||
|
|
||||||
|
if (this.globalUi) {
|
||||||
|
this.globalUi.destroy();
|
||||||
|
}
|
||||||
|
// this.globalUi = new UI('ultrawidify-global-ui', {eventBus: this.eventBus, isGlobal: true});
|
||||||
|
// this.globalUi.enable();
|
||||||
|
// this.globalUi.setUiVisibility(false);
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Ultrawidify: failed to start extension. Error:', e)
|
||||||
|
this.logger.error('setup', "FAILED TO START EXTENSION. Error:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
this.eventBus.unsubscribeAll(this);
|
||||||
|
if (this.pageInfo) {
|
||||||
|
this.pageInfo.destroy();
|
||||||
|
}
|
||||||
|
if (this.keyboardHandler) {
|
||||||
|
this.keyboardHandler.destroy();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,17 +1,25 @@
|
|||||||
import Debug from './conf/Debug.js';
|
import { Runtime } from 'chrome';
|
||||||
import BrowserDetect from './conf/BrowserDetect';
|
import EventBus from '@src/ext/module/EventBus';
|
||||||
import CommsServer from './lib/comms/CommsServer';
|
import Settings from '@src/ext/module/settings/Settings';
|
||||||
import Settings from './lib/Settings';
|
import { ComponentLogger } from '@src/ext/module/logging/ComponentLogger';
|
||||||
import Logger, { baseLoggingOptions } from './lib/Logger';
|
import { BLANK_LOGGER_CONFIG, LogAggregator } from '@src/ext/module/logging/LogAggregator';
|
||||||
|
import CommsServer from '@src/ext/module/comms/CommsServer';
|
||||||
|
import BrowserDetect from '@src/ext/conf/BrowserDetect';
|
||||||
|
import { HostInfo } from '@src/common/interfaces/HostData.interface';
|
||||||
|
import { ExtensionEnvironment } from '@src/common/interfaces/SettingsInterface';
|
||||||
|
import { CommsOrigin } from '@src/ext/module/comms/CommsClient';
|
||||||
|
|
||||||
import { sleep } from '../common/js/utils';
|
|
||||||
|
|
||||||
import { browser } from 'webextension-polyfill-ts';
|
const BASE_LOGGING_STYLES = {
|
||||||
|
'log': 'background-color: #243; color: #4a8',
|
||||||
|
}
|
||||||
|
|
||||||
export default class UWServer {
|
export default class UWServer {
|
||||||
settings: Settings;
|
settings: Settings;
|
||||||
logger: Logger;
|
logger: ComponentLogger;
|
||||||
|
logAggregator: LogAggregator;
|
||||||
comms: CommsServer;
|
comms: CommsServer;
|
||||||
|
eventBus: EventBus;
|
||||||
|
|
||||||
ports: any[] = [];
|
ports: any[] = [];
|
||||||
hasVideos: boolean;
|
hasVideos: boolean;
|
||||||
@ -24,10 +32,40 @@ export default class UWServer {
|
|||||||
'videoSettings': undefined,
|
'videoSettings': undefined,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
eventBusCommands = {
|
||||||
|
'popup-set-selected-tab': {
|
||||||
|
function: (message) => this.setSelectedTab(message.selectedMenu, message.selectedSubitem)
|
||||||
|
},
|
||||||
|
'has-video': {
|
||||||
|
function: (message, context) => this.registerVideo(context.comms.sender)
|
||||||
|
},
|
||||||
|
'noVideo' : {
|
||||||
|
function: (message, context) => this.unregisterVideo(context.comms.sender)
|
||||||
|
},
|
||||||
|
'inject-css': {
|
||||||
|
function: (message, context) => this.injectCss(message.cssString, context.comms.sender)
|
||||||
|
},
|
||||||
|
'eject-css': {
|
||||||
|
function: (message, context) => this.removeCss(message.cssString, context.comms.sender)
|
||||||
|
},
|
||||||
|
'replace-css': {
|
||||||
|
function: (message, context) => this.replaceCss(message.oldCssString, message.newCssString, context.comms.sender)
|
||||||
|
},
|
||||||
|
'get-current-site': {
|
||||||
|
function: (message, context) => this.getCurrentSite(context.comms.sender)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
private gcTimeout: any;
|
private gcTimeout: any;
|
||||||
uiLoggerInitialized: boolean = false;
|
uiLoggerInitialized: boolean = false;
|
||||||
|
|
||||||
|
|
||||||
|
//#region getters
|
||||||
|
get activeTab() {
|
||||||
|
return chrome.tabs.query({currentWindow: true, active: true});
|
||||||
|
}
|
||||||
|
//#endregion
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.setup();
|
this.setup();
|
||||||
}
|
}
|
||||||
@ -35,85 +73,114 @@ export default class UWServer {
|
|||||||
async setup() {
|
async setup() {
|
||||||
try {
|
try {
|
||||||
// logger is the first thing that goes up
|
// logger is the first thing that goes up
|
||||||
const loggingOptions = {
|
const loggingOptions = BLANK_LOGGER_CONFIG;
|
||||||
isBackgroundScript: true,
|
|
||||||
allowLogging: false,
|
|
||||||
useConfFromStorage: true,
|
|
||||||
logAll: true,
|
|
||||||
fileOptions: {
|
|
||||||
enabled: false,
|
|
||||||
},
|
|
||||||
consoleOptions: {
|
|
||||||
enabled: false
|
|
||||||
}
|
|
||||||
};
|
|
||||||
this.logger = new Logger();
|
|
||||||
await this.logger.init(loggingOptions);
|
|
||||||
|
|
||||||
this.settings = new Settings({logger: this.logger});
|
this.logAggregator = new LogAggregator('🔶bg-script🔶');
|
||||||
|
this.logger = new ComponentLogger(this.logAggregator, 'UwServer', {styles: BASE_LOGGING_STYLES});
|
||||||
|
await this.logAggregator.init(loggingOptions);
|
||||||
|
|
||||||
|
this.settings = new Settings({logAggregator: this.logAggregator});
|
||||||
await this.settings.init();
|
await this.settings.init();
|
||||||
this.comms = new CommsServer(this);
|
|
||||||
this.comms.subscribe('show-logger', async () => await this.initUiAndShowLogger());
|
|
||||||
this.comms.subscribe('init-vue', async () => await this.initUi());
|
|
||||||
this.comms.subscribe('uwui-vue-initialized', () => this.uiLoggerInitialized = true);
|
|
||||||
this.comms.subscribe('emit-logs', () => {}); // we don't need to do anything, this gets forwarded to UI content script as is
|
|
||||||
|
|
||||||
browser.tabs.onActivated.addListener((m) => {this.onTabSwitched(m)});
|
this.eventBus = new EventBus({isUWServer: true, commsOrigin: CommsOrigin.Server});
|
||||||
|
|
||||||
|
this.eventBus.subscribeMulti(this.eventBusCommands, this);
|
||||||
|
|
||||||
|
this.comms = new CommsServer(this);
|
||||||
|
this.eventBus.setComms(this.comms);
|
||||||
|
|
||||||
|
chrome.tabs.onActivated.addListener((m) => {this.onTabSwitched(m)});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`Ultrawidify [server]: failed to start. Reason:`, e);
|
console.error(`Ultrawidify [server]: failed to start. Reason:`, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async _promisifyTabsGet(browserObj, tabId){
|
async _promisifyTabsGet(browserObj, tabId){
|
||||||
return new Promise( (resolve, reject) => {
|
return new Promise( (resolve, reject) => {
|
||||||
browserObj.tabs.get(tabId, (tab) => resolve(tab));
|
browserObj.tabs.get(tabId, (tab) => resolve(tab));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//#region CSS management
|
||||||
|
|
||||||
async injectCss(css, sender) {
|
async injectCss(css, sender) {
|
||||||
|
if (!sender?.tab?.id) {
|
||||||
|
// console.warn('invalid injectCss received!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.info('injectCss', 'Trying to inject CSS into tab', sender.tab.id, ', frameId:', sender.frameId, 'css:\n', css)
|
||||||
|
if (!css) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
if (BrowserDetect.firefox || BrowserDetect.edge) {
|
await chrome.scripting.insertCSS({
|
||||||
browser.tabs.insertCSS(sender.tab.id, {code: css, cssOrigin: 'user', frameId: sender.frameId});
|
target: {
|
||||||
} else if (BrowserDetect.anyChromium) {
|
tabId: sender.tab.id,
|
||||||
chrome.tabs.insertCSS(sender.tab.id, {code: css, cssOrigin: 'user', frameId: sender.frameId});
|
frameIds: [
|
||||||
}
|
sender.frameId
|
||||||
|
]
|
||||||
|
},
|
||||||
|
css,
|
||||||
|
origin: "USER"
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.logger.log('error','debug', '[UwServer::injectCss] Error while injecting css:', {error: e, css, sender});
|
this.logger.error('injectCss', 'Error while injecting css:', {error: e, css, sender});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async removeCss(css, sender) {
|
async removeCss(css, sender) {
|
||||||
try {
|
if (!sender?.tab) {
|
||||||
browser.tabs.removeCSS(sender.tab.id, {code: css, cssOrigin: 'user', frameId: sender.frameId});
|
// console.warn('invalid removeCss received!');
|
||||||
} catch (e) {
|
return;
|
||||||
this.logger.log('error','debug', '[UwServer::injectCss] Error while removing css:', {error: e, css, sender});
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
async replaceCss(oldCss, newCss, sender) {
|
try {
|
||||||
if (oldCss !== newCss) {
|
await chrome.scripting.removeCSS({
|
||||||
this.injectCss(newCss, sender);
|
target: {
|
||||||
this.removeCss(oldCss, sender);
|
tabId: sender.tab.id,
|
||||||
|
frameIds: [
|
||||||
|
sender.frameId
|
||||||
|
]
|
||||||
|
},
|
||||||
|
css,
|
||||||
|
origin: "USER"
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
this.logger.error('injectCss', 'Error while removing css:', {error: e, css, sender});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
async replaceCss(oldCss, newCss, sender) {
|
||||||
|
if (!sender?.tab) {
|
||||||
|
// console.warn('invalid replaceCss received!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (oldCss !== newCss) {
|
||||||
|
this.removeCss(oldCss, sender);
|
||||||
|
this.injectCss(newCss, sender);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//#endregion
|
||||||
|
|
||||||
extractHostname(url){
|
extractHostname(url){
|
||||||
var hostname;
|
var hostname;
|
||||||
|
|
||||||
if (!url) {
|
if (!url) {
|
||||||
return "<no url>";
|
return "<no url>";
|
||||||
}
|
}
|
||||||
|
|
||||||
// extract hostname
|
// extract hostname
|
||||||
if (url.indexOf("://") > -1) { //find & remove protocol (http, ftp, etc.) and get hostname
|
if (url.indexOf("://") > -1) { //find & remove protocol (http, ftp, etc.) and get hostname
|
||||||
hostname = url.split('/')[2];
|
hostname = url.split('/')[2];
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
hostname = url.split('/')[0];
|
hostname = url.split('/')[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
hostname = hostname.split(':')[0]; //find & remove port number
|
hostname = hostname.split(':')[0]; //find & remove port number
|
||||||
hostname = hostname.split('?')[0]; //find & remove "?"
|
hostname = hostname.split('?')[0]; //find & remove "?"
|
||||||
|
|
||||||
return hostname;
|
return hostname;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -125,32 +192,36 @@ export default class UWServer {
|
|||||||
|
|
||||||
let tab;
|
let tab;
|
||||||
if (BrowserDetect.firefox) {
|
if (BrowserDetect.firefox) {
|
||||||
tab = await browser.tabs.get(this.currentTabId);
|
tab = await chrome.tabs.get(this.currentTabId);
|
||||||
} else if (BrowserDetect.anyChromium) {
|
} else if (BrowserDetect.anyChromium) {
|
||||||
tab = await this._promisifyTabsGet(chrome, this.currentTabId);
|
tab = await this._promisifyTabsGet(chrome, this.currentTabId);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.currentSite = this.extractHostname(tab.url);
|
this.currentSite = this.extractHostname(tab.url);
|
||||||
this.logger.log('info', 'debug', '[UwServer::onTabSwitched] user switched tab. New site:', this.currentSite);
|
this.logger.info('onTabSwitched', 'user switched tab. New site:', this.currentSite);
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
this.logger.log('error', 'debug', '[UwServer::onTabSwitched] there was a problem getting currnet site:', e)
|
this.logger.info('onTabSwitched', 'there was a problem getting current site:', e)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.selectedSubitem = {
|
this.selectedSubitem = {
|
||||||
'siteSettings': undefined,
|
'siteSettings': undefined,
|
||||||
'videoSettings': undefined,
|
'videoSettings': undefined,
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: change extension icon based on whether there's any videos on current page
|
//TODO: change extension icon based on whether there's any videos on current page
|
||||||
}
|
}
|
||||||
|
|
||||||
registerVideo(sender) {
|
registerVideo(sender) {
|
||||||
this.logger.log('info', 'comms', '[UWServer::registerVideo] Registering video.\nsender:', sender);
|
if (!sender?.tab?.url) {
|
||||||
|
// console.warn('invalid registerVideo received!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.logger.info('registerVideo', 'Registering video.\nsender:', sender);
|
||||||
|
|
||||||
const tabHostname = this.extractHostname(sender.tab.url);
|
const tabHostname = this.extractHostname(sender.tab.url);
|
||||||
const frameHostname = this.extractHostname(sender.url);
|
const frameHostname = this.extractHostname(sender.url);
|
||||||
|
|
||||||
// preveri za osirotele/zastarele vrednosti ter jih po potrebi izbriši
|
// check for orphaned/outdated values and remove them if necessary
|
||||||
// check for orphaned/outdated values and remove them if neccessary
|
|
||||||
if (this.videoTabs[sender.tab.id]?.host != tabHostname) {
|
if (this.videoTabs[sender.tab.id]?.host != tabHostname) {
|
||||||
delete this.videoTabs[sender.tab.id]
|
delete this.videoTabs[sender.tab.id]
|
||||||
} else if(this.videoTabs[sender.tab.id]?.frames[sender.frameId]?.host != frameHostname) {
|
} else if(this.videoTabs[sender.tab.id]?.frames[sender.frameId]?.host != frameHostname) {
|
||||||
@ -179,11 +250,16 @@ export default class UWServer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.log('info', 'comms', '[UWServer::registerVideo] Video registered. current videoTabs:', this.videoTabs);
|
this.logger.info('registerVideo', 'Video registered. current videoTabs:', this.videoTabs);
|
||||||
}
|
}
|
||||||
|
|
||||||
unregisterVideo(sender) {
|
unregisterVideo(sender) {
|
||||||
this.logger.log('info', 'comms', '[UwServer::unregisterVideo] Unregistering video.\nsender:', sender);
|
if (!sender?.tab) {
|
||||||
|
// console.warn('invalid unregisterVideo received!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.info('unregisterVideo', 'Unregistering video.\nsender:', sender);
|
||||||
if (this.videoTabs[sender.tab.id]) {
|
if (this.videoTabs[sender.tab.id]) {
|
||||||
if ( Object.keys(this.videoTabs[sender.tab.id].frames).length <= 1) {
|
if ( Object.keys(this.videoTabs[sender.tab.id].frames).length <= 1) {
|
||||||
delete this.videoTabs[sender.tab.id]
|
delete this.videoTabs[sender.tab.id]
|
||||||
@ -193,75 +269,82 @@ export default class UWServer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.logger.log('info', 'comms', '[UwServer::unregisterVideo] Video has been unregistered. Current videoTabs:', this.videoTabs);
|
this.logger.info('unregisterVideo', 'Video has been unregistered. Current videoTabs:', this.videoTabs);
|
||||||
}
|
}
|
||||||
|
|
||||||
setSelectedTab(menu, subitem) {
|
setSelectedTab(menu, subitem) {
|
||||||
this.logger.log('info', 'comms', '[UwServer::setSelectedTab] saving selected tab for', menu, ':', subitem);
|
this.logger.info('setSelectedTab', 'saving selected tab for', menu, ':', subitem);
|
||||||
this.selectedSubitem[menu] = subitem;
|
this.selectedSubitem[menu] = subitem;
|
||||||
}
|
}
|
||||||
|
|
||||||
async initUi() {
|
async getCurrentSite(sender: Runtime.MessageSender) {
|
||||||
try {
|
// if (!sender?.tab) {
|
||||||
if (BrowserDetect.firefox) {
|
// console.warn('invalid unregisterVideo received!');
|
||||||
await browser.tabs.executeScript({
|
// return;
|
||||||
file: '/ext/uw-ui.js',
|
// }
|
||||||
allFrames: true,
|
const site = await this.getVideoTab();
|
||||||
});
|
|
||||||
} else if (BrowserDetect.anyChromium) {
|
// Don't propagate 'INVALID SITE' to the popup.
|
||||||
await new Promise<void>( resolve =>
|
if (site.host === 'INVALID SITE') {
|
||||||
chrome.tabs.executeScript({
|
this.logger.info('getCurrentSite', 'Host is not valid — no info for current tab.');
|
||||||
file: '/ext/uw-ui.js',
|
return;
|
||||||
allFrames: true,
|
|
||||||
}, () => resolve())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('Ultrawidify [server]: UI setup failed. While problematic, this problem shouldn\'t completely crash the extension.');
|
|
||||||
this.logger.log('ERROR', 'uwbg', 'UI initialization failed. Reason:', e);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
async initUiAndShowLogger() {
|
const tabHostname = await this.getCurrentTabHostname();
|
||||||
try {
|
this.logger.info('getCurrentSite', 'Returning data:', {site, tabHostname});
|
||||||
// this implementation is less than optimal and very hacky, but it should work
|
console.info('get-current-site : returning data:', {site, tabHostname});
|
||||||
// just fine for our use case.
|
|
||||||
this.uiLoggerInitialized = false;
|
|
||||||
|
|
||||||
await this.initUi();
|
this.eventBus.send(
|
||||||
|
'set-current-site',
|
||||||
await new Promise<void>( async (resolve, reject) => {
|
{
|
||||||
// if content script doesn't give us a response within 5 seconds, something is
|
site,
|
||||||
// obviously wrong and we stop waiting,
|
tabHostname,
|
||||||
|
},
|
||||||
// oh and btw, resolve/reject do not break the loops, so we need to do that
|
{
|
||||||
// ourselves:
|
comms: {
|
||||||
// https://stackoverflow.com/questions/55207256/will-resolve-in-promise-loop-break-loop-iteration
|
forwardTo: 'popup'
|
||||||
let isRejected = false;
|
|
||||||
setTimeout( async () => {isRejected = true; reject()}, 5000);
|
|
||||||
|
|
||||||
// check whether UI has been initiated on the FE. If it was, we resolve the
|
|
||||||
// promise and off we go
|
|
||||||
while (!isRejected) {
|
|
||||||
if (this.uiLoggerInitialized) {
|
|
||||||
resolve();
|
|
||||||
return; // remember the bit about resolve() not breaking the loop?
|
|
||||||
}
|
|
||||||
await sleep(100);
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
} catch (e) {
|
)
|
||||||
console.warn('Ultrawidify [server]: failed to set up logger UI. While problematic, this problem shouldn\'t completely crash the extension.');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCurrentTab() {
|
async getCurrentTab() {
|
||||||
return (await browser.tabs.query({active: true, currentWindow: true}))[0];
|
return (await chrome.tabs.query({active: true, currentWindow: true}))[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private populateFrameVideoStatus(tabId: number, hostnames: string[]) {
|
||||||
|
const out: HostInfo[] = [];
|
||||||
|
|
||||||
|
for (const host of hostnames) {
|
||||||
|
let video = {hasVideo: false, minEnvironment: ExtensionEnvironment.Normal, maxEnvironment: ExtensionEnvironment.Fullscreen};
|
||||||
|
|
||||||
|
for (const frameKey in this.videoTabs[tabId].frames) {
|
||||||
|
const frame = this.videoTabs[tabId].frames[frameKey];
|
||||||
|
|
||||||
|
if (frame.host === host) {
|
||||||
|
video.hasVideo = true;
|
||||||
|
if (frame.environment > video.maxEnvironment) {
|
||||||
|
video.maxEnvironment = frame.environment;
|
||||||
|
}
|
||||||
|
if (frame.environment < video.minEnvironment) {
|
||||||
|
video.minEnvironment = frame.environment;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out.push({
|
||||||
|
host,
|
||||||
|
...video,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _lastVideoTabData: any | undefined;
|
||||||
async getVideoTab() {
|
async getVideoTab() {
|
||||||
// friendly reminder: if current tab doesn't have a video,
|
// friendly reminder: if current tab doesn't have a video,
|
||||||
// there won't be anything in this.videoTabs[this.currentTabId]
|
// there won't be anything in this.videoTabs[this.currentTabId]
|
||||||
|
|
||||||
const ctab = await this.getCurrentTab();
|
const ctab = await this.getCurrentTab();
|
||||||
@ -270,46 +353,85 @@ export default class UWServer {
|
|||||||
return {
|
return {
|
||||||
host: 'INVALID SITE',
|
host: 'INVALID SITE',
|
||||||
frames: [],
|
frames: [],
|
||||||
|
hostnames: [],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.videoTabs[ctab.id]) {
|
const hostnames = await this.comms.listUniqueFrameHosts();
|
||||||
// if video is older than PageInfo's video rescan period (+ 4000ms of grace),
|
|
||||||
// we clean it up from videoTabs[tabId].frames array.
|
// this probably means we're inside a problematic page
|
||||||
const ageLimit = Date.now() - this.settings.active.pageInfo.timeouts.rescan - 4000;
|
if (!this.videoTabs[ctab.id]) {
|
||||||
try {
|
return this._lastVideoTabData ?? {
|
||||||
for (const key in this.videoTabs[ctab.id].frames) {
|
host: 'INVALID SITE',
|
||||||
if (this.videoTabs[ctab.id].frames[key].registerTime < ageLimit) {
|
frames: [],
|
||||||
delete this.videoTabs[ctab.id].frames[key];
|
hostnames: [],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
// something went wrong. There's prolly no frames.
|
// if video is older than PageInfo's video rescan period (+ 4000ms of grace),
|
||||||
return {
|
// we clean it up from videoTabs[tabId].frames array.
|
||||||
host: this.extractHostname(ctab.url),
|
const ageLimit = Date.now() - this.settings.active.pageInfo.timeouts.rescan - 4000;
|
||||||
frames: [],
|
try {
|
||||||
selected: this.selectedSubitem
|
for (const key in this.videoTabs[ctab.id].frames) {
|
||||||
|
if (this.videoTabs[ctab.id].frames[key].registerTime < ageLimit) {
|
||||||
|
delete this.videoTabs[ctab.id].frames[key];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// something went wrong. There's prolly no frames.
|
||||||
|
// this probably shouldn't ever run.
|
||||||
return {
|
return {
|
||||||
...this.videoTabs[ctab.id],
|
|
||||||
host: this.extractHostname(ctab.url),
|
host: this.extractHostname(ctab.url),
|
||||||
selected: this.selectedSubitem
|
hostnames: [],
|
||||||
};
|
frames: [],
|
||||||
|
selected: this.selectedSubitem
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// return something more or less empty if this tab doesn't have
|
// Ensure hostnames with videos come before hostnames without videos
|
||||||
// a video registered for it
|
// Also ensure hostnames are sorted alphabetically
|
||||||
return {
|
const populatedHostnames = this.populateFrameVideoStatus(ctab.id, hostnames);
|
||||||
|
populatedHostnames.sort((a: HostInfo, b: HostInfo) => {
|
||||||
|
return a.hasVideo === b.hasVideo
|
||||||
|
? a.host === b.host ? 0 : a.host < b.host ? -1 : 1
|
||||||
|
: a.hasVideo < b.hasVideo ? 1 : -1;
|
||||||
|
});
|
||||||
|
|
||||||
|
this._lastVideoTabData = {
|
||||||
host: this.extractHostname(ctab.url),
|
host: this.extractHostname(ctab.url),
|
||||||
frames: [],
|
hostnames: populatedHostnames.map(x => x.host), // todo: try eliminating this
|
||||||
|
populatedHostnames: populatedHostnames,
|
||||||
selected: this.selectedSubitem
|
selected: this.selectedSubitem
|
||||||
}
|
};
|
||||||
|
|
||||||
|
return this._lastVideoTabData;
|
||||||
}
|
}
|
||||||
|
|
||||||
// chrome shitiness mitigation
|
async getCurrentTabHostname() {
|
||||||
sendUnmarkPlayer(message) {
|
const activeTab = await this.activeTab;
|
||||||
this.comms.sendUnmarkPlayer(message);
|
|
||||||
|
if (!activeTab || activeTab.length < 1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = activeTab[0].url;
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
|
console.log('no URL for active tab:', activeTab[0].url);
|
||||||
|
}
|
||||||
|
|
||||||
|
var hostname;
|
||||||
|
|
||||||
|
if (url.indexOf("://") > -1) { //find & remove protocol (http, ftp, etc.) and get hostname
|
||||||
|
hostname = url.split('/')[2];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
hostname = url.split('/')[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
hostname = hostname.split(':')[0]; //find & remove port number
|
||||||
|
hostname = hostname.split('?')[0]; //find & remove "?"
|
||||||
|
|
||||||
|
return hostname;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -179,7 +179,7 @@ var ActionList = {
|
|||||||
page: true,
|
page: true,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'set-ExtensionMode': {
|
'set-extension-mode': {
|
||||||
name: 'Set extension mode',
|
name: 'Set extension mode',
|
||||||
args: [{
|
args: [{
|
||||||
name: 'Enable',
|
name: 'Enable',
|
||||||
@ -259,4 +259,4 @@ var ActionList = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ActionList;
|
export default ActionList;
|
||||||
|
|||||||
@ -5,13 +5,21 @@ if (process.env.CHANNEL !== 'stable') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function detectEdgeUA() {
|
function detectEdgeUA() {
|
||||||
return /Edg\/(\.?[0-9]*)*$/.test(window.navigator.userAgent);
|
try {
|
||||||
|
return /Edg\/(\.?[0-9]*)*$/.test(window.navigator.userAgent);
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getBrowserObj() {
|
function getBrowserObj() {
|
||||||
return process.env.BROWSER === 'firefox' ? browser : chrome;
|
return process.env.BROWSER === 'firefox' ? browser : chrome;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getRuntime() {
|
||||||
|
return process.env.BROWSER === 'firefox' ? browser.runtime : chrome.runtime;
|
||||||
|
}
|
||||||
|
|
||||||
function getURL(url) {
|
function getURL(url) {
|
||||||
return process.env.BROWSER === 'firefox' ? browser.runtime.getURL(url) : chrome.runtime.getURL(url);
|
return process.env.BROWSER === 'firefox' ? browser.runtime.getURL(url) : chrome.runtime.getURL(url);
|
||||||
}
|
}
|
||||||
@ -25,11 +33,12 @@ const BrowserDetect = {
|
|||||||
processEnvChannel: process.env.CHANNEL,
|
processEnvChannel: process.env.CHANNEL,
|
||||||
isEdgeUA: detectEdgeUA(),
|
isEdgeUA: detectEdgeUA(),
|
||||||
browserObj: getBrowserObj(),
|
browserObj: getBrowserObj(),
|
||||||
|
runtime: getRuntime(),
|
||||||
getURL: (url) => getURL(url),
|
getURL: (url) => getURL(url),
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.env.CHANNEL !== 'stable') {
|
if (process.env.CHANNEL !== 'stable') {
|
||||||
console.info("BrowserDetect loaded:\n\nprocess.env.BROWSER:", process.env.BROWSER, "\nExporting BrowserDetect:", BrowserDetect);
|
console.info("BrowserDetect loaded:\n\nprocess.env.BROWSER:", process.env.BROWSER, "\nExporting BrowserDetect:", BrowserDetect);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default BrowserDetect;
|
export default BrowserDetect;
|
||||||
|
|||||||
@ -1,226 +0,0 @@
|
|||||||
// How to use:
|
|
||||||
// version: {ExtensionConf object, but only properties that get overwritten}
|
|
||||||
import StretchType from '../../common/enums/StretchType.enum';
|
|
||||||
import ExtensionMode from '../../common/enums/ExtensionMode.enum';
|
|
||||||
import VideoAlignmentType from '../../common/enums/VideoAlignmentType.enum';
|
|
||||||
import BrowserDetect from './BrowserDetect';
|
|
||||||
|
|
||||||
const ExtensionConfPatch = [
|
|
||||||
{
|
|
||||||
forVersion: '4.5.0',
|
|
||||||
sites: {
|
|
||||||
"www.wakanim.tv": {
|
|
||||||
type: 'community',
|
|
||||||
DOM: {
|
|
||||||
player: {
|
|
||||||
manual: true,
|
|
||||||
querySelectors: "#jwplayer-container",
|
|
||||||
additionalCss: "",
|
|
||||||
useRelativeAncestor: false,
|
|
||||||
playerNodeCss: "",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
forVersion: '4.5.1',
|
|
||||||
updateFn: (userOptions, defaultOptions) => {
|
|
||||||
for (const site in userOptions.sites) {
|
|
||||||
try {
|
|
||||||
delete userOptions[sites].autoarPreventConditions
|
|
||||||
} catch (e) {
|
|
||||||
// doesn't matter if site doesn't have that option,
|
|
||||||
// everything is still fine
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
forVersion: '4.5.1.1',
|
|
||||||
updateFn: (userOptions, defaultOptions) => {
|
|
||||||
if (!userOptions.sites['streamable.com']) {
|
|
||||||
userOptions.sites['streamable.com'] = {
|
|
||||||
mode: 3,
|
|
||||||
autoar: 3,
|
|
||||||
type: 'official',
|
|
||||||
stretch: -1,
|
|
||||||
videoAlignment: -1,
|
|
||||||
keyboardShortcutsEnabled: 0,
|
|
||||||
css: ".player {text-align: left}"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (!userOptions.sites['streamable.com'].css) {
|
|
||||||
userOptions.sites['streamable.com'].css = '.player {text-align: left}'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
forVersion: '4.5.1.3',
|
|
||||||
updateFn: (userOptions, defaultOptions) => {
|
|
||||||
try {
|
|
||||||
userOptions.sites['wwww.disneyplus.com']['css'] = ".hudson-container {\n height: 100%;\n}";
|
|
||||||
} catch (e) {
|
|
||||||
// do nothing if disney+ is missing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
forVersion: '5.0.1',
|
|
||||||
updateFn: (userOptions, defaultOptions) => {
|
|
||||||
try {
|
|
||||||
userOptions.mitigations = {
|
|
||||||
zoomLimit: {
|
|
||||||
enabled: BrowserDetect.edge || BrowserDetect.isEdgeUA,
|
|
||||||
limit: 0.997,
|
|
||||||
fullscreenOnly: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
forVersion: '5.0.1.1',
|
|
||||||
updateFn: (userOptions, defaultOptions) => {
|
|
||||||
try {
|
|
||||||
userOptions.mitigations = {
|
|
||||||
zoomLimit: {
|
|
||||||
enabled: true,
|
|
||||||
limit: 0.997,
|
|
||||||
fullscreenOnly: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
forVersion: '5.0.2',
|
|
||||||
updateFn: (userOptions, defaultOptions) => {
|
|
||||||
try {
|
|
||||||
if (! userOptions.mitigations) {
|
|
||||||
userOptions.mitigations = {
|
|
||||||
zoomLimit: {
|
|
||||||
enabled: true,
|
|
||||||
limit: 0.997,
|
|
||||||
fullscreenOnly: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (BrowserDetect.chrome) {
|
|
||||||
userOptions.mitigations = {
|
|
||||||
zoomLimit: {
|
|
||||||
enabled: true,
|
|
||||||
limit: 0.997,
|
|
||||||
fullscreenOnly: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
forVersion: '5.0.4',
|
|
||||||
updateFn: (userOptions, defaultOptions) => {
|
|
||||||
userOptions.sites['www.disneyplus.com'].DOM.player = {
|
|
||||||
... userOptions.sites['www.disneyplus.com'].DOM.player,
|
|
||||||
querySelectors: ".btm-media-client-element",
|
|
||||||
useRelativeAncestor: true,
|
|
||||||
videoAncestor: 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
forVersion: '5.0.5',
|
|
||||||
sites: {
|
|
||||||
"app.plex.tv": {
|
|
||||||
mode: 3,
|
|
||||||
autoar: 3,
|
|
||||||
type: "community",
|
|
||||||
stretch: -1,
|
|
||||||
videoAlignment: -1,
|
|
||||||
keyboardShortcutsEnabled: 0,
|
|
||||||
DOM: {
|
|
||||||
player: {
|
|
||||||
manual: false,
|
|
||||||
querySelectors: "",
|
|
||||||
additionalCss: "",
|
|
||||||
useRelativeAncestor: false,
|
|
||||||
playerNodeCss: ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
css: "body {\n background-color: #000;\n}\n\n.application {\n background-color: #000;\n}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
forVersion: '5.0.6',
|
|
||||||
sites: {
|
|
||||||
"metaivi.com": {
|
|
||||||
mode: 0,
|
|
||||||
autoar: 0,
|
|
||||||
type: "community",
|
|
||||||
stretch: -1,
|
|
||||||
videoAlignment: -1,
|
|
||||||
DOM: {
|
|
||||||
video: {
|
|
||||||
manual: false,
|
|
||||||
querySelectors: "",
|
|
||||||
additionalCss: "position: absolute !important;"
|
|
||||||
},
|
|
||||||
player: {
|
|
||||||
manual: false,
|
|
||||||
querySelectors: "",
|
|
||||||
additionalCss: "",
|
|
||||||
useRelativeAncestor: false,
|
|
||||||
playerNodeCss: ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"css": ""
|
|
||||||
},
|
|
||||||
"piped.kavin.rocks": {
|
|
||||||
mode: 0,
|
|
||||||
autoar: 0,
|
|
||||||
type: 'community',
|
|
||||||
autoarFallback: 0,
|
|
||||||
stretch: 0,
|
|
||||||
videoAlignment: -1,
|
|
||||||
keyboardShortcutsEnabled: 0,
|
|
||||||
DOM: {
|
|
||||||
player: {
|
|
||||||
manual: false,
|
|
||||||
querySelectors: "",
|
|
||||||
additionalCss: "",
|
|
||||||
useRelativeAncestor: false,
|
|
||||||
playerNodeCss: ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
css: ".shaka-video-container {\n flex-direction: column !important;\n}"
|
|
||||||
},
|
|
||||||
},
|
|
||||||
updateFn: (userOptions, defaultOptions) => {
|
|
||||||
// 5.0.5 initially incorrectly had app.plex.tv marked as 'user-added'
|
|
||||||
// when 'user-added' is generally reserved for marking sites with user-
|
|
||||||
// changed configuration. Site patches submitted by community should have
|
|
||||||
// 'community' type. extConfPatch for 5.0.5 was also retroactively corrected.
|
|
||||||
userOptions.sites['app.plex.tv'].type = 'community';
|
|
||||||
userOptions.sites['piped.kavin.rocks'] = {
|
|
||||||
mode: 0,
|
|
||||||
autoar: 0,
|
|
||||||
type: 'community',
|
|
||||||
autoarFallback: 0,
|
|
||||||
stretch: 0,
|
|
||||||
videoAlignment: -1,
|
|
||||||
keyboardShortcutsEnabled: 0,
|
|
||||||
DOM: {
|
|
||||||
player: {
|
|
||||||
manual: false,
|
|
||||||
querySelectors: "",
|
|
||||||
additionalCss: "",
|
|
||||||
useRelativeAncestor: false,
|
|
||||||
playerNodeCss: ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
css: ".shaka-video-container {\n flex-direction: column !important;\n}"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
|
|
||||||
export default ExtensionConfPatch;
|
|
||||||
539
src/ext/conf/ExtConfPatches.ts
Normal file
539
src/ext/conf/ExtConfPatches.ts
Normal file
@ -0,0 +1,539 @@
|
|||||||
|
// How to use:
|
||||||
|
// version: {ExtensionConf object, but only properties that get overwritten}
|
||||||
|
import AspectRatioType from '@src/common/enums/AspectRatioType.enum';
|
||||||
|
import CropModePersistence from '@src/common/enums/CropModePersistence.enum';
|
||||||
|
import EmbeddedContentSettingsOverridePolicy from '@src/common/enums/EmbeddedContentSettingsOverridePolicy.enum';
|
||||||
|
import ExtensionMode from '@src/common/enums/ExtensionMode.enum';
|
||||||
|
import { InputHandlingMode } from '@src/common/enums/InputHandlingMode.enum';
|
||||||
|
import LegacyExtensionMode from '@src/common/enums/LegacyExtensionMode.enum';
|
||||||
|
import { PlayerDetectionMode } from '@src/common/enums/PlayerDetectionMode.enum';
|
||||||
|
import { SiteSupportLevel } from '@src/common/enums/SiteSupportLevel.enum';
|
||||||
|
import SettingsInterface from '@src/common/interfaces/SettingsInterface';
|
||||||
|
import { _cp } from '@src/common/utils/_cp';
|
||||||
|
import { update } from 'lodash';
|
||||||
|
|
||||||
|
|
||||||
|
const ExtensionConfPatch = Object.freeze([
|
||||||
|
{
|
||||||
|
forVersion: '6.2.4',
|
||||||
|
updateFn: (userOptions: any, defaultOptions, logger?) => {
|
||||||
|
for (const site in userOptions.sites) {
|
||||||
|
(userOptions as any).sites[site].enableUI = {
|
||||||
|
fullscreen: LegacyExtensionMode.Default,
|
||||||
|
theater: LegacyExtensionMode.Default,
|
||||||
|
normal: LegacyExtensionMode.Default,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
userOptions.sites['@global'].enableUI = {
|
||||||
|
fullscreen: userOptions.ui.inPlayer.enabled ? LegacyExtensionMode.Enabled : LegacyExtensionMode.Disabled,
|
||||||
|
theater: LegacyExtensionMode.Enabled,
|
||||||
|
normal: (userOptions.ui.inPlayer.enabled && !userOptions.ui.inPlayer.enabledFullscreenOnly) ? LegacyExtensionMode.Enabled : LegacyExtensionMode.Disabled
|
||||||
|
}
|
||||||
|
userOptions.sites['@empty'].enableUI = {
|
||||||
|
fullscreen: LegacyExtensionMode.Default,
|
||||||
|
theater: LegacyExtensionMode.Default,
|
||||||
|
normal: LegacyExtensionMode.Default,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
forVersion: '6.2.6',
|
||||||
|
updateFn: (userOptions: any, defaultOptions, logger?) => {
|
||||||
|
console.log('[ultrawidify] Migrating settings — applying patches for version 6.2.6');
|
||||||
|
|
||||||
|
if (!userOptions.commands) {
|
||||||
|
userOptions.commands = {
|
||||||
|
zoom: [],
|
||||||
|
crop: [],
|
||||||
|
stretch: [],
|
||||||
|
pan: [],
|
||||||
|
internal: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
userOptions.commands.zoom = [{
|
||||||
|
action: 'change-zoom',
|
||||||
|
label: 'Zoom +5%',
|
||||||
|
arguments: {
|
||||||
|
zoom: 0.05
|
||||||
|
},
|
||||||
|
shortcut: {
|
||||||
|
key: 'z',
|
||||||
|
code: 'KeyY',
|
||||||
|
ctrlKey: false,
|
||||||
|
metaKey: false,
|
||||||
|
altKey: false,
|
||||||
|
shiftKey: false,
|
||||||
|
onKeyUp: true,
|
||||||
|
onKeyDown: false,
|
||||||
|
},
|
||||||
|
internalOnly: true,
|
||||||
|
actionId: 'change-zoom-10in'
|
||||||
|
}, {
|
||||||
|
action: 'change-zoom',
|
||||||
|
label: 'Zoom -5%',
|
||||||
|
arguments: {
|
||||||
|
zoom: -0.05
|
||||||
|
},
|
||||||
|
shortcut: {
|
||||||
|
key: 'u',
|
||||||
|
code: 'KeyU',
|
||||||
|
ctrlKey: false,
|
||||||
|
metaKey: false,
|
||||||
|
altKey: false,
|
||||||
|
shiftKey: false,
|
||||||
|
onKeyUp: true,
|
||||||
|
onKeyDown: false,
|
||||||
|
},
|
||||||
|
internalOnly: true,
|
||||||
|
actionId: 'change-zoom-10out'
|
||||||
|
}, {
|
||||||
|
action: 'set-zoom',
|
||||||
|
label: 'Reset zoom',
|
||||||
|
arguments: {
|
||||||
|
zoom: 1,
|
||||||
|
},
|
||||||
|
internalOnly: true,
|
||||||
|
actionId: 'set-zoom-reset'
|
||||||
|
}];
|
||||||
|
|
||||||
|
delete (userOptions as any).actions;
|
||||||
|
|
||||||
|
userOptions.dev = {
|
||||||
|
loadFromSnapshot: false
|
||||||
|
};
|
||||||
|
userOptions.ui.dev = {
|
||||||
|
aardDebugOverlay: {
|
||||||
|
showOnStartup: false,
|
||||||
|
showDetectionDetails: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const newZoomActions = [{
|
||||||
|
action: 'set-zoom',
|
||||||
|
label: 'Reset zoom',
|
||||||
|
shortcut: {
|
||||||
|
key: 'r',
|
||||||
|
code: 'KeyR',
|
||||||
|
ctrlKey: false,
|
||||||
|
metaKey: false,
|
||||||
|
altKey: false,
|
||||||
|
shiftKey: true,
|
||||||
|
onKeyUp: true,
|
||||||
|
onKeyDown: false,
|
||||||
|
},
|
||||||
|
arguments: {
|
||||||
|
zoom: 1
|
||||||
|
},
|
||||||
|
internalOnly: true,
|
||||||
|
actionId: 'set-zoom-reset'
|
||||||
|
}, {
|
||||||
|
action: 'set-ar-zoom',
|
||||||
|
label: 'Automatic',
|
||||||
|
comment: 'Automatically detect aspect ratio and zoom accordingly',
|
||||||
|
arguments: {
|
||||||
|
type: AspectRatioType.Automatic
|
||||||
|
},
|
||||||
|
shortcut: {
|
||||||
|
key: 'a',
|
||||||
|
code: 'KeyA',
|
||||||
|
ctrlKey: false,
|
||||||
|
metaKey: false,
|
||||||
|
altKey: false,
|
||||||
|
shiftKey: true,
|
||||||
|
onKeyUp: true,
|
||||||
|
onKeyDown: false,
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
action: 'set-ar-zoom',
|
||||||
|
label: 'Cycle',
|
||||||
|
comment: 'Cycle through zoom options',
|
||||||
|
arguments: {
|
||||||
|
type: AspectRatioType.Cycle
|
||||||
|
},
|
||||||
|
shortcut: {
|
||||||
|
key: 'c',
|
||||||
|
code: 'KeyC',
|
||||||
|
ctrlKey: false,
|
||||||
|
metaKey: false,
|
||||||
|
altKey: false,
|
||||||
|
shiftKey: true,
|
||||||
|
onKeyUp: true,
|
||||||
|
onKeyDown: false,
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
action: 'set-ar-zoom',
|
||||||
|
label: '21:9',
|
||||||
|
comment: 'Zoom for 21:9 aspect ratio (1:2.39)',
|
||||||
|
arguments: {
|
||||||
|
type: AspectRatioType.Fixed,
|
||||||
|
ratio: 2.39
|
||||||
|
},
|
||||||
|
shortcut: {
|
||||||
|
key: 'd',
|
||||||
|
code: 'KeyD',
|
||||||
|
ctrlKey: false,
|
||||||
|
metaKey: false,
|
||||||
|
altKey: false,
|
||||||
|
shiftKey: true,
|
||||||
|
onKeyUp: false,
|
||||||
|
onKeyDown: true,
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
action: 'set-ar-zoom',
|
||||||
|
label: '18:9',
|
||||||
|
comment: 'Zoom for 18:9 aspect ratio (1:2)',
|
||||||
|
arguments: {
|
||||||
|
type: AspectRatioType.Fixed,
|
||||||
|
ratio: 1.78
|
||||||
|
},
|
||||||
|
shortcut: {
|
||||||
|
key: 's',
|
||||||
|
code: 'KeyS',
|
||||||
|
ctrlKey: false,
|
||||||
|
metaKey: false,
|
||||||
|
altKey: false,
|
||||||
|
shiftKey: true,
|
||||||
|
onKeyUp: false,
|
||||||
|
onKeyDown: true,
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
action: 'set-ar-zoom',
|
||||||
|
label: '32:9',
|
||||||
|
comment: 'Zoom for 32:9 aspect ratio',
|
||||||
|
arguments: {
|
||||||
|
type: AspectRatioType.Fixed,
|
||||||
|
ratio: 3.56
|
||||||
|
},
|
||||||
|
}];
|
||||||
|
|
||||||
|
const compareShortcuts = (a: any, b: any) => {
|
||||||
|
if (!a || !b) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.key === b.key && b.code === b.code && a.ctrlKey == b.ctrlKey && a.shiftKey == b.shiftKey && a.metaKey == a.metaKey && a.altKey == b.altKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasConflict = (shortcut: any) => {
|
||||||
|
for (const ct in userOptions.commands) {
|
||||||
|
for (const command of userOptions.commands[ct]) {
|
||||||
|
if (compareShortcuts(shortcut, command.shortcut)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const zoomAction of newZoomActions) {
|
||||||
|
if (
|
||||||
|
!userOptions.commands.zoom.find(
|
||||||
|
x => x.action === zoomAction.action
|
||||||
|
&& x.arguments?.type === zoomAction.arguments?.type
|
||||||
|
&& x.arguments?.ratio === zoomAction.arguments?.ratio
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
userOptions.commands.zoom.push({
|
||||||
|
...zoomAction,
|
||||||
|
shortcut: hasConflict(zoomAction.shortcut) ? undefined : zoomAction.shortcut
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
forVersion: '6.3.92',
|
||||||
|
updateFn: (userOptions: any) => {
|
||||||
|
// applyToEmbeddedContent is now an enum, and also no longer optional
|
||||||
|
for (const site in userOptions.sites) {
|
||||||
|
if (userOptions.sites[site].applyToEmbeddedContent === undefined) {
|
||||||
|
userOptions.sites[site].applyToEmbeddedContent = EmbeddedContentSettingsOverridePolicy.UseAsDefault;
|
||||||
|
} else {
|
||||||
|
userOptions.sites[site].applyToEmbeddedContent = userOptions.sites[site].applyToEmbeddedContent ? EmbeddedContentSettingsOverridePolicy.Always : EmbeddedContentSettingsOverridePolicy.Never;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
forVersion: '6.3.93',
|
||||||
|
updateFn: (userOptions: SettingsInterface, defaultOptions: SettingsInterface) => {
|
||||||
|
(userOptions as any).arDetect.polling = defaultOptions.aard.polling;
|
||||||
|
(userOptions as any).arDetect.subtitles = defaultOptions.aard.subtitles;
|
||||||
|
(userOptions as any).arDetect.autoDisable = defaultOptions.aard.autoDisable;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
forVersion: '6.3.98',
|
||||||
|
updateFn: (userOptions: SettingsInterface, defaultOptions: SettingsInterface) => {
|
||||||
|
(userOptions as any).arDetect.letterboxOrientationScan = defaultOptions.aard.letterboxOrientationScan;
|
||||||
|
(userOptions as any).arDetect.edgeDetection = defaultOptions.aard.edgeDetection;
|
||||||
|
(userOptions as any).arDetect.subtitles = defaultOptions.aard.subtitles;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
forVersion: '6.3.98',
|
||||||
|
updateFn: (userOptions: SettingsInterface, defaultOptions: SettingsInterface) => {
|
||||||
|
userOptions.aard = defaultOptions.aard;
|
||||||
|
userOptions.aardLegacy = defaultOptions.aardLegacy;
|
||||||
|
delete (userOptions as any).arDetect;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
forVersion: '6.3.994',
|
||||||
|
updateFn: (userOptions: SettingsInterface, defaultOptions: SettingsInterface, logger?) => {
|
||||||
|
const convertLegacyExtensionMode = (option: {normal: LegacyExtensionMode, theater: LegacyExtensionMode, fullscreen: LegacyExtensionMode}) => {
|
||||||
|
if (typeof option === 'number') {
|
||||||
|
logger.warn('updateFn', 'This option is not an object, which suggests it has already been converted. Skipping conversion. Raw value of option:', option);
|
||||||
|
return option;
|
||||||
|
}
|
||||||
|
logger.log(
|
||||||
|
'updateFn',
|
||||||
|
`\nconverting option ——— normal: ${LegacyExtensionMode[option.normal]} theater: ${LegacyExtensionMode[option.normal]} fs: ${LegacyExtensionMode[option.fullscreen]}`, '\nraw obj:', option
|
||||||
|
);
|
||||||
|
if (option.normal === LegacyExtensionMode.Enabled) {
|
||||||
|
return ExtensionMode.All;
|
||||||
|
}
|
||||||
|
if (option.theater === LegacyExtensionMode.Enabled) {
|
||||||
|
return ExtensionMode.Theater;
|
||||||
|
}
|
||||||
|
if (option.fullscreen === LegacyExtensionMode.Enabled) {
|
||||||
|
return ExtensionMode.FullScreen;
|
||||||
|
}
|
||||||
|
if (option.fullscreen === LegacyExtensionMode.Disabled) {
|
||||||
|
return ExtensionMode.Disabled;
|
||||||
|
}
|
||||||
|
return ExtensionMode.Default;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key in userOptions.sites) {
|
||||||
|
logger.log('updateFn', '\n\n ... migrating default enable-state for site', key);
|
||||||
|
userOptions.sites[key].enable = convertLegacyExtensionMode(userOptions.sites[key].enable as any);
|
||||||
|
userOptions.sites[key].enableAard = convertLegacyExtensionMode(userOptions.sites[key].enableAard as any);
|
||||||
|
|
||||||
|
if (key === '@global') {
|
||||||
|
userOptions.sites['@global'].enableKeyboard = userOptions.kbm.enabled && userOptions.kbm.keyboardEnabled ? InputHandlingMode.Enabled : InputHandlingMode.Disabled;
|
||||||
|
} else {
|
||||||
|
userOptions.sites[key].enableKeyboard = InputHandlingMode.Default;
|
||||||
|
}
|
||||||
|
|
||||||
|
userOptions.sites[key].enableUI = convertLegacyExtensionMode(
|
||||||
|
userOptions.sites[key].enableUI ?? (key === '@global' ? ExtensionMode.FullScreen : ExtensionMode.Default) as any
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.log('updateFn', 'migrated site', key, userOptions.sites[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
forVersion: '6.3.995',
|
||||||
|
updateFn: (userOptions: SettingsInterface, defaultOptions: SettingsInterface, logger?) => {
|
||||||
|
if (!userOptions.ui) {
|
||||||
|
userOptions.ui = defaultOptions.ui
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
forVersion: '6.3.996',
|
||||||
|
updateFn: (userOptions: SettingsInterface, defaultOptions: SettingsInterface, logger?) => {
|
||||||
|
for (const site in userOptions.sites) {
|
||||||
|
const siteData = userOptions.sites[site];
|
||||||
|
logger.log('updateFn', 'migrating settings for', site, ' — persistCSA?', siteData.persistCSA, 'typeof persistCSA?', typeof siteData.persistCSA, 'does domconfig exist?', siteData.DOMConfig);
|
||||||
|
|
||||||
|
if (typeof siteData.persistCSA !== 'number') {
|
||||||
|
userOptions.sites[site].persistCSA = CropModePersistence.Default;
|
||||||
|
} else {
|
||||||
|
userOptions.sites[site].persistCSA = userOptions.sites[site].persistCSA ?? CropModePersistence.Default;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userOptions.sites[site].type as any === 'user-added' || userOptions.sites[site].type === 'user-defined') {
|
||||||
|
userOptions.sites[site].type = SiteSupportLevel.UserDefined;
|
||||||
|
}
|
||||||
|
if (userOptions.sites[site].type as any === 'no-support') {
|
||||||
|
userOptions.sites[site].type = SiteSupportLevel.Unknown;
|
||||||
|
}
|
||||||
|
if (userOptions.sites[site].defaultType as any === 'user-added' || userOptions.sites[site].defaultType === 'user-defined') {
|
||||||
|
userOptions.sites[site].type = SiteSupportLevel.UserDefined;
|
||||||
|
}
|
||||||
|
if (userOptions.sites[site].defaultType as any === 'no-support') {
|
||||||
|
userOptions.sites[site].defaultType = SiteSupportLevel.Unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (siteData.activeDOMConfig?.startsWith('community') || siteData.activeDOMConfig === 'official' || siteData.activeDOMConfig === 'empty' || siteData.activeDOMConfig === 'auto') {
|
||||||
|
siteData.activeDOMConfig = `@${siteData.activeDOMConfig}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const domConf in siteData.DOMConfig) {
|
||||||
|
logger.log('updateFn', "Updating domconf", domConf);
|
||||||
|
const oldConf = userOptions.sites[site].DOMConfig[domConf] as any;
|
||||||
|
logger.log('updateFn', "——— oldConf:", oldConf);
|
||||||
|
|
||||||
|
const newConf: any = {
|
||||||
|
type: oldConf.type ?? userOptions.sites[site].type,
|
||||||
|
elements: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (newConf.type === 'user-added' || newConf.type === 'user-defined') {
|
||||||
|
newConf.type = SiteSupportLevel.UserDefined;
|
||||||
|
}
|
||||||
|
if (newConf.type === 'no-support') {
|
||||||
|
newConf.type = SiteSupportLevel.Unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldConf.elements?.player) {
|
||||||
|
newConf.elements['player'] = {
|
||||||
|
detectionMode: oldConf?.elements?.player?.manual ? (
|
||||||
|
oldConf?.elements?.player?.querySelectors.trim() ? PlayerDetectionMode.QuerySelectors : PlayerDetectionMode.AncestorIndex
|
||||||
|
) : PlayerDetectionMode.Auto,
|
||||||
|
allowAutoFallback: true,
|
||||||
|
ancestorIndex: oldConf?.elements?.player?.parentIndex,
|
||||||
|
querySelectors: oldConf?.elements?.player?.querySelectors,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
newConf.elements['player'] = {
|
||||||
|
detectionMode: PlayerDetectionMode.Auto,
|
||||||
|
allowAutoFallback: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (oldConf.customCss) {
|
||||||
|
newConf.customCss = oldConf.customCss;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldConf.elements?.video) {
|
||||||
|
newConf.elements['video'] = {
|
||||||
|
type: oldConf.type ?? siteData.type,
|
||||||
|
elements: {
|
||||||
|
video: {
|
||||||
|
playerDetectionMode: oldConf?.elements?.video?.manual ? PlayerDetectionMode.QuerySelectors : PlayerDetectionMode.Auto,
|
||||||
|
allowAutoFallback: true,
|
||||||
|
querySelectors: oldConf?.elements?.video?.querySelectors,
|
||||||
|
customCSS: oldConf?.elements?.video?.customCss,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// migrate names — official and community options get @ at the start
|
||||||
|
let domConfName = domConf;
|
||||||
|
if (domConfName.startsWith('community') || domConfName === 'official' || domConfName === 'empty' || domConfName === 'auto') {
|
||||||
|
domConfName = `@${domConfName}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (domConfName !== domConf) {
|
||||||
|
logger.warn(`updateFn`, '\n\nnaming for default domConf has changed. Old:', domConf, 'new:', domConfName);
|
||||||
|
delete userOptions.sites[site].DOMConfig[domConf];
|
||||||
|
}
|
||||||
|
|
||||||
|
userOptions.sites[site].DOMConfig[domConfName] = newConf;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// set new defaults for global and empty:
|
||||||
|
userOptions.sites['@global'].DOMConfig = {
|
||||||
|
'@auto': {
|
||||||
|
type: SiteSupportLevel.Unknown,
|
||||||
|
elements: {
|
||||||
|
player: {
|
||||||
|
detectionMode: PlayerDetectionMode.Auto,
|
||||||
|
allowAutoFallback: true,
|
||||||
|
},
|
||||||
|
video: {
|
||||||
|
detectionMode: PlayerDetectionMode.Auto,
|
||||||
|
allowAutoFallback: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
userOptions.sites['@global'].activeDOMConfig = '@auto';
|
||||||
|
userOptions.sites['@empty'].DOMConfig = {
|
||||||
|
'@empty': {
|
||||||
|
type: SiteSupportLevel.UserDefined,
|
||||||
|
elements: {
|
||||||
|
player: {
|
||||||
|
detectionMode: PlayerDetectionMode.Auto,
|
||||||
|
allowAutoFallback: true,
|
||||||
|
},
|
||||||
|
video: {
|
||||||
|
detectionMode: PlayerDetectionMode.Auto,
|
||||||
|
allowAutoFallback: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
userOptions.sites['@empty'].activeDOMConfig = '@empty';
|
||||||
|
|
||||||
|
logger.log('updateFn', 'Migration complete. New site settings:', userOptions.sites);
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
forVersion: '6.3.997',
|
||||||
|
updateFn: (userOptions: SettingsInterface, defaultOptions: SettingsInterface, logger?) => {
|
||||||
|
|
||||||
|
// default zoom key combinations that involved the 'shift' key should have
|
||||||
|
// shortcut.key in uppercase, but they didn't.
|
||||||
|
for (const command of userOptions.commands?.zoom) {
|
||||||
|
if (command.shortcut) {
|
||||||
|
if (command.shortcut.shiftKey) {
|
||||||
|
if (command.shortcut.key.toUpperCase() === command.shortcut.code.charAt(3)) {
|
||||||
|
command.shortcut.key = command.shortcut.key.toUpperCase();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
forVersion: '6.3.998',
|
||||||
|
updateFn: (userOptions: SettingsInterface, defaultOptions: SettingsInterface, logger?) => {
|
||||||
|
if (!userOptions.sites["www.amazon.com"]) {
|
||||||
|
userOptions.sites["www.amazon.com"] = _cp(defaultOptions.sites["www.amazon.com"] );
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userOptions.commands?.zoom) {
|
||||||
|
const firstFixed = userOptions.commands?.zoom.findIndex(x => x.arguments.type === AspectRatioType.Fixed);
|
||||||
|
|
||||||
|
if (firstFixed === -1) {
|
||||||
|
userOptions.commands.zoom.push({
|
||||||
|
action: 'set-ar-zoom',
|
||||||
|
label: 'Cover',
|
||||||
|
comment: 'Covers the entire screen, cropping as much as needed',
|
||||||
|
arguments: {
|
||||||
|
type: AspectRatioType.Cover
|
||||||
|
},
|
||||||
|
shortcut: {
|
||||||
|
key: 'w',
|
||||||
|
code: 'KeyW',
|
||||||
|
ctrlKey: false,
|
||||||
|
metaKey: false,
|
||||||
|
altKey: false,
|
||||||
|
shiftKey: true,
|
||||||
|
onKeyUp: true,
|
||||||
|
onKeyDown: false,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
userOptions.commands.zoom.splice(firstFixed, 0, {
|
||||||
|
action: 'set-ar-zoom',
|
||||||
|
label: 'Cover',
|
||||||
|
comment: 'Covers the entire screen, cropping as much as needed',
|
||||||
|
arguments: {
|
||||||
|
type: AspectRatioType.Cover
|
||||||
|
},
|
||||||
|
shortcut: {
|
||||||
|
key: 'w',
|
||||||
|
code: 'KeyW',
|
||||||
|
ctrlKey: false,
|
||||||
|
metaKey: false,
|
||||||
|
altKey: false,
|
||||||
|
shiftKey: true,
|
||||||
|
onKeyUp: true,
|
||||||
|
onKeyDown: false,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
]);
|
||||||
|
|
||||||
|
|
||||||
|
export default ExtensionConfPatch;
|
||||||
File diff suppressed because it is too large
Load Diff
5
src/ext/enum/run-level.enum.ts
Normal file
5
src/ext/enum/run-level.enum.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export enum RunLevel {
|
||||||
|
Off = 0,
|
||||||
|
UIOnly = 1,
|
||||||
|
CustomCSSActive = 2
|
||||||
|
}
|
||||||
@ -1,348 +0,0 @@
|
|||||||
import Debug from '../conf/Debug';
|
|
||||||
import PlayerData from './video-data/PlayerData';
|
|
||||||
import ExtensionMode from '../../common/enums/ExtensionMode.enum';
|
|
||||||
import Logger from './Logger';
|
|
||||||
import PageInfo from './video-data/PageInfo';
|
|
||||||
import Settings from './Settings';
|
|
||||||
import VideoData from './video-data/VideoData';
|
|
||||||
|
|
||||||
if(process.env.CHANNEL !== 'stable'){
|
|
||||||
console.info("Loading ActionHandler");
|
|
||||||
}
|
|
||||||
|
|
||||||
class ActionHandler {
|
|
||||||
logger: Logger;
|
|
||||||
pageInfo: PageInfo;
|
|
||||||
settings: Settings;
|
|
||||||
|
|
||||||
|
|
||||||
inputs: string[] = ['input', 'select', 'button', 'textarea'];
|
|
||||||
keyboardLocalDisabled: boolean = false;
|
|
||||||
|
|
||||||
keyUpActions: any[] = [];
|
|
||||||
keyDownActions: any[] = [];
|
|
||||||
mouseMoveActions: any[] = [];
|
|
||||||
mouseScrollUpActions: any[] = [];
|
|
||||||
mouseScrollDownActions: any[] = [];
|
|
||||||
mouseEnterActions: any[] = [];
|
|
||||||
mouseLeaveActions: any[] = [];
|
|
||||||
|
|
||||||
|
|
||||||
constructor(pageInfo) {
|
|
||||||
this.logger = pageInfo.logger;
|
|
||||||
this.pageInfo = pageInfo;
|
|
||||||
this.settings = pageInfo.settings;
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
this.logger.log('info', 'debug', "[ActionHandler::init] starting init");
|
|
||||||
|
|
||||||
this.keyUpActions = [];
|
|
||||||
this.keyDownActions = [];
|
|
||||||
this.mouseMoveActions = [];
|
|
||||||
this.mouseScrollUpActions = [];
|
|
||||||
this.mouseScrollDownActions = [];
|
|
||||||
this.mouseEnterActions = [];
|
|
||||||
this.mouseLeaveActions = [];
|
|
||||||
|
|
||||||
var ths = this;
|
|
||||||
|
|
||||||
var actions;
|
|
||||||
try {
|
|
||||||
if (this.settings.active.sites[window.location.hostname].actions) {
|
|
||||||
actions = this.settings.active.sites[window.location.hostname].actions;
|
|
||||||
} else {
|
|
||||||
actions = this.settings.active.actions;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
actions = this.settings.active.actions;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
for (var action of actions) {
|
|
||||||
if (!action.scopes) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
for (var scope in action.scopes) {
|
|
||||||
if (! action.scopes[scope].shortcut) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var shortcut = action.scopes[scope].shortcut[0];
|
|
||||||
if (shortcut.onKeyDown) {
|
|
||||||
this.keyDownActions.push({
|
|
||||||
shortcut: shortcut,
|
|
||||||
cmd: action.cmd,
|
|
||||||
scope: scope,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (shortcut.onKeyUp) {
|
|
||||||
this.keyUpActions.push({
|
|
||||||
shortcut: shortcut,
|
|
||||||
cmd: action.cmd,
|
|
||||||
scope: scope,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (shortcut.onScrollUp) {
|
|
||||||
this.mouseScrollUpActions.push({
|
|
||||||
shortcut: shortcut,
|
|
||||||
cmd: action.cmd,
|
|
||||||
scope: scope,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (shortcut.onScrollDown) {
|
|
||||||
this.mouseScrollDownActions.push({
|
|
||||||
shortcut: shortcut,
|
|
||||||
cmd: action.cmd,
|
|
||||||
scope: scope,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (shortcut.onMouseEnter) {
|
|
||||||
this.mouseEnterActions.push({
|
|
||||||
shortcut: shortcut,
|
|
||||||
cmd: action.cmd,
|
|
||||||
scope: scope,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (shortcut.onMouseLeave) {
|
|
||||||
this.mouseLeaveActions.push({
|
|
||||||
shortcut: shortcut,
|
|
||||||
cmd: action.cmd,
|
|
||||||
scope: scope,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (shortcut.onMouseMove) {
|
|
||||||
this.mouseMoveActions.push({
|
|
||||||
shortcut: shortcut,
|
|
||||||
cmd: action.cmd,
|
|
||||||
scope: scope,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// events should be handled in handleEvent function. We need to do things this
|
|
||||||
// way, otherwise we can't remove event listener
|
|
||||||
// https://stackoverflow.com/a/19507086
|
|
||||||
document.addEventListener('keydown', this );
|
|
||||||
document.addEventListener('keyup', this );
|
|
||||||
|
|
||||||
this.pageInfo.setActionHandler(this);
|
|
||||||
|
|
||||||
this.logger.log('info', 'debug', "[ActionHandler::init] initialization complete");
|
|
||||||
}
|
|
||||||
|
|
||||||
handleEvent(event) {
|
|
||||||
switch(event.type) {
|
|
||||||
case 'keydown':
|
|
||||||
this.handleKeydown(event);
|
|
||||||
break;
|
|
||||||
case 'keyup':
|
|
||||||
this.handleKeyup(event);
|
|
||||||
break;
|
|
||||||
case 'mousemove':
|
|
||||||
this.handleMouseMove(event);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
destroy() {
|
|
||||||
document.removeEventListener('keydown', this);
|
|
||||||
document.removeEventListener('keyup', this);
|
|
||||||
}
|
|
||||||
|
|
||||||
registerHandleMouse(videoData) {
|
|
||||||
this.logger.log('info', ['actionHandler', 'mousemove'], "[ActionHandler::registerHandleMouse] registering handle mouse for videodata:", videoData.id)
|
|
||||||
|
|
||||||
var ths = this;
|
|
||||||
if (videoData.player && videoData.player.element) {
|
|
||||||
videoData.player.element.addEventListener('mousemove', (event) => ths.handleMouseMove(event, videoData));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
unregisterHandleMouse(videoData) {
|
|
||||||
var ths = this;
|
|
||||||
if (videoData.player && videoData.player.element) {
|
|
||||||
videoData.player.element.removeEventListener('mousemove', (event) => ths.handleMouseMove(event, videoData));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setKeyboardLocal(state) {
|
|
||||||
if (state === ExtensionMode.Enabled) {
|
|
||||||
this.keyboardLocalDisabled = false;
|
|
||||||
} else if (state === ExtensionMode.Disabled) {
|
|
||||||
this.keyboardLocalDisabled = true;
|
|
||||||
}
|
|
||||||
// don't do shit on invalid value of state
|
|
||||||
}
|
|
||||||
|
|
||||||
preventAction(event) {
|
|
||||||
var activeElement = document.activeElement;
|
|
||||||
|
|
||||||
if (this.logger.canLog('keyboard')) {
|
|
||||||
this.logger.pause(); // temp disable to avoid recursing;
|
|
||||||
const preventAction = this.preventAction(event);
|
|
||||||
this.logger.resume(); // undisable
|
|
||||||
|
|
||||||
this.logger.log('info', 'keyboard', "[ActionHandler::preventAction] Testing whether we're in a textbox or something. Detailed rundown of conditions:\n" +
|
|
||||||
"\nis tag one of defined inputs? (yes->prevent):", this.inputs.indexOf(activeElement.tagName.toLocaleLowerCase()) !== -1,
|
|
||||||
"\nis role = textbox? (yes -> prevent):", activeElement.getAttribute("role") === "textbox",
|
|
||||||
"\nis type === 'text'? (yes -> prevent):", activeElement.getAttribute("type") === "text",
|
|
||||||
"\nevent.target.isContentEditable? (yes -> prevent):", event.target.isContentEditable,
|
|
||||||
"\nis keyboard local disabled? (yes -> prevent):", this.keyboardLocalDisabled,
|
|
||||||
"\nis keyboard enabled in settings? (no -> prevent)", this.settings.keyboardShortcutsEnabled(window.location.hostname),
|
|
||||||
"\nwill the action be prevented? (yes -> prevent)", preventAction,
|
|
||||||
"\n-----------------{ extra debug info }-------------------",
|
|
||||||
"\ntag name? (lowercase):", activeElement.tagName, activeElement.tagName.toLocaleLowerCase(),
|
|
||||||
"\nrole:", activeElement.getAttribute('role'),
|
|
||||||
"\ntype:", activeElement.getAttribute('type'),
|
|
||||||
"\ninsta-fail inputs:", this.inputs,
|
|
||||||
"\nevent:", event,
|
|
||||||
"\nevent.target:", event.target
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.keyboardLocalDisabled) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (!this.settings.keyboardShortcutsEnabled(window.location.hostname)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (this.inputs.indexOf(activeElement.tagName.toLocaleLowerCase()) !== -1) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (activeElement.getAttribute("role") === "textbox") {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (event.target.isContentEditable) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (activeElement.getAttribute("type") === "text") {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
isLatin(key) {
|
|
||||||
return 'abcdefghijklmnopqrstuvwxyz,.-+1234567890'.indexOf(key.toLocaleLowerCase()) !== -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
isActionMatchStandard(shortcut, event) {
|
|
||||||
return shortcut.key === event.key &&
|
|
||||||
shortcut.ctrlKey === event.ctrlKey &&
|
|
||||||
shortcut.metaKey === event.metaKey &&
|
|
||||||
shortcut.altKey === event.altKey &&
|
|
||||||
shortcut.shiftKey === event.shiftKey
|
|
||||||
}
|
|
||||||
isActionMatchKeyCode(shortcut, event) {
|
|
||||||
return shortcut.code === event.code &&
|
|
||||||
shortcut.ctrlKey === event.ctrlKey &&
|
|
||||||
shortcut.metaKey === event.metaKey &&
|
|
||||||
shortcut.altKey === event.altKey &&
|
|
||||||
shortcut.shiftKey === event.shiftKey
|
|
||||||
}
|
|
||||||
|
|
||||||
isActionMatch(shortcut, event, isLatin = true) {
|
|
||||||
// ASCII and symbols fall back to key code matching, because we don't know for sure that
|
|
||||||
// regular matching by key is going to work
|
|
||||||
return isLatin ?
|
|
||||||
this.isActionMatchStandard(shortcut, event) :
|
|
||||||
this.isActionMatchStandard(shortcut, event) || this.isActionMatchKeyCode(shortcut, event);
|
|
||||||
}
|
|
||||||
|
|
||||||
execAction(actions, event, videoData?: VideoData) {
|
|
||||||
this.logger.log('info', 'keyboard', "%c[ActionHandler::execAction] Trying to find and execute action for event. Actions/event: ", "color: #ff0", actions, event);
|
|
||||||
|
|
||||||
const isLatin = event.key ? this.isLatin(event.key) : true;
|
|
||||||
|
|
||||||
for (var action of actions) {
|
|
||||||
if (this.isActionMatch(action.shortcut, event, isLatin)) {
|
|
||||||
this.logger.log('info', 'keyboard', "%c[ActionHandler::execAction] found an action associated with keypress/event: ", "color: #ff0", action);
|
|
||||||
|
|
||||||
for (var cmd of action.cmd) {
|
|
||||||
if (action.scope === 'page') {
|
|
||||||
if (cmd.action === "set-ar") {
|
|
||||||
this.pageInfo.setAr({type: cmd.arg, ratio: cmd.customArg});
|
|
||||||
} else if (cmd.action === "change-zoom") {
|
|
||||||
this.pageInfo.zoomStep(cmd.arg);
|
|
||||||
} else if (cmd.action === "set-zoom") {
|
|
||||||
this.pageInfo.setZoom(cmd.arg);
|
|
||||||
} else if (cmd.action === "set-stretch") {
|
|
||||||
this.pageInfo.setStretchMode(cmd.arg);
|
|
||||||
} else if (cmd.action === "toggle-pan") {
|
|
||||||
this.pageInfo.setPanMode(cmd.arg)
|
|
||||||
} else if (cmd.action === "pan") {
|
|
||||||
if (videoData) {
|
|
||||||
videoData.panHandler(event, true);
|
|
||||||
}
|
|
||||||
} else if (cmd.action === 'set-keyboard') {
|
|
||||||
this.setKeyboardLocal(cmd.arg);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let site = action.scope === 'site' ? window.location.hostname : '@global';
|
|
||||||
|
|
||||||
if (cmd.action === "set-stretch") {
|
|
||||||
this.settings.active.sites[site].stretch = cmd.arg;
|
|
||||||
} else if (cmd.action === "set-alignment") {
|
|
||||||
this.settings.active.sites[site].videoAlignment = cmd.arg;
|
|
||||||
} else if (cmd.action === "set-extension-mode") {
|
|
||||||
this.settings.active.sites[site].mode = cmd.arg;
|
|
||||||
} else if (cmd.action === "set-autoar-mode") {
|
|
||||||
this.settings.active.sites[site].autoar = cmd.arg;
|
|
||||||
} else if (cmd.action === 'set-keyboard') {
|
|
||||||
this.settings.active.sites[site].keyboardShortcutsEnabled = cmd.arg;
|
|
||||||
} else if (cmd.action === 'set-ar-persistence') {
|
|
||||||
this.settings.active.sites[site]['cropModePersistence'] = cmd.arg;
|
|
||||||
this.pageInfo.setArPersistence(cmd.arg);
|
|
||||||
this.settings.saveWithoutReload();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cmd.action !== 'set-ar-persistence') {
|
|
||||||
this.settings.save();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// če smo našli dejanje za to tipko, potem ne preiskujemo naprej
|
|
||||||
// if we found an action for this key, we stop searching for a match
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
handleKeyup(event) {
|
|
||||||
this.logger.log('info', 'keyboard', "%c[ActionHandler::handleKeyup] we pressed a key: ", "color: #ff0", event.key , " | keyup: ", event.keyup, "event:", event);
|
|
||||||
|
|
||||||
if (this.preventAction(event)) {
|
|
||||||
this.logger.log('info', 'keyboard', "[ActionHandler::handleKeyup] we are in a text box or something. Doing nothing.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.execAction(this.keyUpActions, event);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleKeydown(event) {
|
|
||||||
this.logger.log('info', 'keyboard', "%c[ActionHandler::handleKeydown] we pressed a key: ", "color: #ff0", event.key , " | keydown: ", event.keydown, "event:", event)
|
|
||||||
|
|
||||||
if (this.preventAction(event)) {
|
|
||||||
this.logger.log('info', 'keyboard', "[ActionHandler::handleKeydown] we are in a text box or something. Doing nothing.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.execAction(this.keyDownActions, event);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleMouseMove(event, videoData?: VideoData) {
|
|
||||||
this.logger.log('info', 'keyboard', "[ActionHandler::handleMouseMove] mouse move is being handled.\nevent:", event, "\nvideo data:", videoData);
|
|
||||||
videoData?.panHandler(event);
|
|
||||||
this.execAction(this.mouseMoveActions, event, videoData)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
if(process.env.CHANNEL !== 'stable'){
|
|
||||||
console.info("ActionHandler loaded");
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ActionHandler;
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
class Interface {
|
|
||||||
constructor(videoData) {
|
|
||||||
this.conf = videoData;
|
|
||||||
this.player = videoData.player;
|
|
||||||
}
|
|
||||||
|
|
||||||
injectUi() {
|
|
||||||
this.detectorDiv = document.createElement('div');
|
|
||||||
this.uiRoot = document.createElement('div');
|
|
||||||
this.detectorDiv.appendChild(this.uiRoot);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,189 +0,0 @@
|
|||||||
class PlayerPickerHelper {
|
|
||||||
constructor (settings, callbacks) {
|
|
||||||
this.settings = settings;
|
|
||||||
this.videos = document.selectElementsByTagName('video');
|
|
||||||
this.selectedParentIndex = this.findPlayerForVideos(settings, this.videos)[0];
|
|
||||||
this.savedCss = [];
|
|
||||||
this.markVideos();
|
|
||||||
this.markIndexForAll(index);
|
|
||||||
this.markInitialQuerySelectors();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
*
|
|
||||||
* Internal functions
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
saveBorder(element) {
|
|
||||||
if (this.savedCss.findIndex(x => x.element === element) !== -1) {
|
|
||||||
this.savedCss.push({element: element, border: element.style.border});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
restoreBorders() {
|
|
||||||
for (const e of this.savedCss) {
|
|
||||||
e.element.style.border = e.border;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
findPlayerForVideos(settings, videos) {
|
|
||||||
const playerIndexes = [];
|
|
||||||
for (const v of videos) {
|
|
||||||
playerIndexes.push(this.findPlayerForVideo(settings, v));
|
|
||||||
}
|
|
||||||
return playerIndexes;
|
|
||||||
}
|
|
||||||
|
|
||||||
findPlayerForVideo(settings, video) {
|
|
||||||
const host = window.location.hostname;
|
|
||||||
let element = video.parentNode;
|
|
||||||
|
|
||||||
if (this.settings.active.sites[host]
|
|
||||||
&& this.settings.active.sites[host].DOM
|
|
||||||
&& this.settings.active.sites[host].DOM.player
|
|
||||||
&& this.settings.active.sites[host].DOM.player.manual) {
|
|
||||||
if (this.settings.active.sites[host].DOM.player.useRelativeAncestor
|
|
||||||
&& this.settings.active.sites[host].DOM.player.videoAncestor) {
|
|
||||||
|
|
||||||
return this.settings.active.sites[host].DOM.player.videoAncestor;
|
|
||||||
} else if (this.settings.active.sites[host].DOM.player.querySelectors) {
|
|
||||||
const allSelectors = document.querySelectorAll(this.settings.active.sites[host].DOM.player.querySelectors);
|
|
||||||
let elementIndex = 1;
|
|
||||||
while (element && !this.collectionHas(allSelectors, element)) {
|
|
||||||
element = element.parentNode;
|
|
||||||
elementIndex++;
|
|
||||||
}
|
|
||||||
return elementIndex;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let elementIndex = 0;
|
|
||||||
|
|
||||||
var trustCandidateAfterGrows = 2; // if candidate_width or candidate_height increases in either dimensions this many
|
|
||||||
// times, we say we found our player. (This number ignores weird elements)
|
|
||||||
// in case our <video> is bigger than player in one dimension but smaller in the other
|
|
||||||
// if site is coded properly, player can't be wider than that
|
|
||||||
var candidate_width = Math.max(element.offsetWidth, window.innerWidth);
|
|
||||||
var candidate_height = Math.max(element.offsetHeight, window.innerHeight);
|
|
||||||
var playerCandidateNode = element;
|
|
||||||
|
|
||||||
// if we haven't found element using fancy methods, we resort to the good old fashioned way
|
|
||||||
var grows = trustCandidateAfterGrows;
|
|
||||||
while(element != undefined){
|
|
||||||
// odstranimo čudne elemente, ti bi pokvarili zadeve
|
|
||||||
// remove weird elements, those would break our stuff
|
|
||||||
if ( element.offsetWidth == 0 || element.offsetHeight == 0){
|
|
||||||
element = element.parentNode;
|
|
||||||
elementIndex++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( element.offsetHeight <= candidate_height &&
|
|
||||||
element.offsetWidth <= candidate_width ){
|
|
||||||
|
|
||||||
// if we're in fullscreen, we only consider elements that are exactly as big as the monitor.
|
|
||||||
if( ! isFullScreen ||
|
|
||||||
(element.offsetWidth == window.innerWidth && element.offsetHeight == window.innerHeight) ){
|
|
||||||
|
|
||||||
playerCandidateNode = element;
|
|
||||||
candidate_width = element.offsetWidth;
|
|
||||||
candidate_height = element.offsetHeight;
|
|
||||||
|
|
||||||
grows = trustCandidateAfterGrows;
|
|
||||||
|
|
||||||
this.logger.log('info', 'debug', "Found new candidate for player. Dimensions: w:", candidate_width, "h:",candidate_height, "node:", playerCandidateNode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if(grows --<= 0){
|
|
||||||
this.logger.log('info', 'playerDetect', "Current element grew in comparrison to the child. We probably found the player. breaking loop, returning current result");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
element = element.parentNode;
|
|
||||||
elementIndex++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return elementIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
markVideos() {
|
|
||||||
for (const v of this.videos) {
|
|
||||||
this.markVideo(v);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
markVideo(video) {
|
|
||||||
this.saveBorder(video);
|
|
||||||
video.style.border = "1px solid #00f";
|
|
||||||
}
|
|
||||||
|
|
||||||
markIndexForAll(index){
|
|
||||||
for (const v of this.videos) {
|
|
||||||
this.markIndex(index, v);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
markIndex(index, video) {
|
|
||||||
el = video.parentNode;
|
|
||||||
while (index --> 1) {
|
|
||||||
el = el.parentNode;
|
|
||||||
}
|
|
||||||
this.saveBorder(el);
|
|
||||||
el.style.border = "1px solid #88f";
|
|
||||||
}
|
|
||||||
markInitialQuerySelectors() {
|
|
||||||
try {
|
|
||||||
if (this.settings.active.sites[host].DOM.player.querySelectors.trim()) {
|
|
||||||
this.markQuerySelectorMatches(this.settings.active.sites[host].DOM.player.querySelectors);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// nothing to see here. something in that if spaghett is undefined, which causes
|
|
||||||
// everything to fail. Since this means we've got zero query string matches to mark,
|
|
||||||
// we just ignore the failure
|
|
||||||
}
|
|
||||||
}
|
|
||||||
markQuerySelectorMatches(qsString) {
|
|
||||||
const allSelectors = document.querySelectorAll(qsString);
|
|
||||||
for (e of allSelectors) {
|
|
||||||
this.saveBorder(e);
|
|
||||||
e.style.border = "1px dashed fd2";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
markQsPlayerDetection(qsString, index, video) {
|
|
||||||
let element = video.parentNode;
|
|
||||||
let elementIndex = 1;
|
|
||||||
|
|
||||||
const allSelectors = document.querySelectorAll(qsString);
|
|
||||||
while (element && !this.collectionHas(allSelectors, element)) {
|
|
||||||
element = element.parentNode;
|
|
||||||
elementIndex++;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.saveBorder(element)
|
|
||||||
if (elementIndex > index) {
|
|
||||||
element.style.border = "2px solid #f00"
|
|
||||||
} else if (elementIndex === index) {
|
|
||||||
element.style.border = "2px solid #027a5c"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* Function that actually interface with playerpicker and do stuff
|
|
||||||
*
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
setQuerySelectors(querySelectorString) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default PlayerPickerHelper
|
|
||||||
@ -1,594 +0,0 @@
|
|||||||
import Debug from '../conf/Debug';
|
|
||||||
import currentBrowser from '../conf/BrowserDetect';
|
|
||||||
import ExtensionConf from '../conf/ExtensionConf';
|
|
||||||
import ExtensionMode from '../../common/enums/ExtensionMode.enum';
|
|
||||||
import ObjectCopy from './ObjectCopy';
|
|
||||||
import StretchType from '../../common/enums/StretchType.enum';
|
|
||||||
import VideoAlignmentType from '../../common/enums/VideoAlignmentType.enum';
|
|
||||||
import ExtensionConfPatch from '../conf/ExtConfPatches';
|
|
||||||
import CropModePersistence from '../../common/enums/CropModePersistence.enum';
|
|
||||||
import BrowserDetect from '../conf/BrowserDetect';
|
|
||||||
import Logger from './Logger';
|
|
||||||
import SettingsInterface from '../../common/interfaces/SettingsInterface';
|
|
||||||
import { browser } from 'webextension-polyfill-ts';
|
|
||||||
|
|
||||||
if(process.env.CHANNEL !== 'stable'){
|
|
||||||
console.info("Loading Settings");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class Settings {
|
|
||||||
//#region flags
|
|
||||||
useSync: boolean = false;
|
|
||||||
version: string;
|
|
||||||
//#endregion
|
|
||||||
|
|
||||||
//#region helper classes
|
|
||||||
logger: Logger;
|
|
||||||
//#endregion
|
|
||||||
|
|
||||||
//#region data
|
|
||||||
default: SettingsInterface; // default settings
|
|
||||||
active: SettingsInterface; // currently active settings
|
|
||||||
//#endregion
|
|
||||||
|
|
||||||
//#region callbacks
|
|
||||||
onSettingsChanged: any;
|
|
||||||
afterSettingsSaved: any;
|
|
||||||
//#endregion
|
|
||||||
|
|
||||||
constructor(options) {
|
|
||||||
// Options: activeSettings, updateCallback, logger
|
|
||||||
this.logger = options?.logger;
|
|
||||||
this.onSettingsChanged = options?.onSettingsChanged;
|
|
||||||
this.afterSettingsSaved = options?.afterSettingsSaved;
|
|
||||||
this.active = options?.activeSettings ?? undefined;
|
|
||||||
this.default = ExtensionConf;
|
|
||||||
this.default['version'] = this.getExtensionVersion();
|
|
||||||
|
|
||||||
browser.storage.onChanged.addListener((changes, area) => {this.storageChangeListener(changes, area)});
|
|
||||||
}
|
|
||||||
|
|
||||||
storageChangeListener(changes, area) {
|
|
||||||
if (!changes.uwSettings) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.logger?.log('info', 'settings', "[Settings::<storage/on change>] Settings have been changed outside of here. Updating active settings. Changes:", changes, "storage area:", area);
|
|
||||||
// if (changes['uwSettings'] && changes['uwSettings'].newValue) {
|
|
||||||
// this.logger?.log('info', 'settings',"[Settings::<storage/on change>] new settings object:", JSON.parse(changes.uwSettings.newValue));
|
|
||||||
// }
|
|
||||||
const parsedSettings = JSON.parse(changes.uwSettings.newValue);
|
|
||||||
this.setActive(parsedSettings);
|
|
||||||
|
|
||||||
this.logger?.log('info', 'debug', 'Does parsedSettings.preventReload exist?', parsedSettings.preventReload, "Does callback exist?", !!this.onSettingsChanged);
|
|
||||||
|
|
||||||
if (!parsedSettings.preventReload && this.onSettingsChanged) {
|
|
||||||
try {
|
|
||||||
this.onSettingsChanged();
|
|
||||||
|
|
||||||
this.logger?.log('info', 'settings', '[Settings] Update callback finished.')
|
|
||||||
} catch (e) {
|
|
||||||
this.logger?.log('error', 'settings', "[Settings] CALLING UPDATE CALLBACK FAILED. Reason:", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (this.afterSettingsSaved) {
|
|
||||||
this.afterSettingsSaved();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static getExtensionVersion(): string {
|
|
||||||
return browser.runtime.getManifest().version;
|
|
||||||
}
|
|
||||||
getExtensionVersion(): string {
|
|
||||||
return Settings.getExtensionVersion();
|
|
||||||
}
|
|
||||||
|
|
||||||
compareExtensionVersions(a, b) {
|
|
||||||
let aa = a.split('.');
|
|
||||||
let bb = b.split('.');
|
|
||||||
|
|
||||||
if (+aa[0] !== +bb[0]) {
|
|
||||||
// difference on first digit
|
|
||||||
return +aa[0] - +bb[0];
|
|
||||||
} if (+aa[1] !== +bb[1]) {
|
|
||||||
// first digit same, difference on second digit
|
|
||||||
return +aa[1] - +bb[1];
|
|
||||||
} if (+aa[2] !== +bb[2]) {
|
|
||||||
return +aa[2] - +bb[2];
|
|
||||||
// first two digits the same, let's check the third digit
|
|
||||||
} else {
|
|
||||||
// fourth digit is optional. When not specified, 0 is implied
|
|
||||||
// btw, ++(aa[3] || 0) - ++(bb[3] || 0) doesn't work
|
|
||||||
|
|
||||||
// Since some things are easier if we actually have a value for
|
|
||||||
// the fourth digit, we turn a possible undefined into a zero
|
|
||||||
aa[3] = aa[3] === undefined ? 0 : aa[3];
|
|
||||||
bb[3] = bb[3] === undefined ? 0 : bb[3];
|
|
||||||
|
|
||||||
// also, the fourth digit can start with a letter.
|
|
||||||
// versions that start with a letter are ranked lower than
|
|
||||||
// versions x.x.x.0
|
|
||||||
if (
|
|
||||||
(isNaN(+aa[3]) && !isNaN(+bb[3]))
|
|
||||||
|| (!isNaN(+aa[3]) && isNaN(+bb[3]))
|
|
||||||
) {
|
|
||||||
return isNaN(+aa[3]) ? -1 : 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// at this point, either both version numbers are a NaN or
|
|
||||||
// both versions are a number.
|
|
||||||
if (!isNaN(+aa[3])) {
|
|
||||||
return +aa[3] - +bb[3];
|
|
||||||
}
|
|
||||||
|
|
||||||
// letters have their own hierarchy:
|
|
||||||
// dev < a < b < rc
|
|
||||||
let av = this.getPrereleaseVersionHierarchy(aa[3]);
|
|
||||||
let bv = this.getPrereleaseVersionHierarchy(bb[3]);
|
|
||||||
|
|
||||||
if (av !== bv) {
|
|
||||||
return av - bv;
|
|
||||||
} else {
|
|
||||||
return +(aa[3].replace(/\D/g,'')) - +(bb[3].replace(/\D/g, ''));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getPrereleaseVersionHierarchy(version) {
|
|
||||||
if (version.startsWith('dev')) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
if (version.startsWith('a')) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
if (version.startsWith('b')) {
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
return 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
sortConfPatches(patchesIn) {
|
|
||||||
return patchesIn.sort( (a, b) => this.compareExtensionVersions(a.forVersion, b.forVersion));
|
|
||||||
}
|
|
||||||
|
|
||||||
findFirstNecessaryPatch(version, extconfPatches) {
|
|
||||||
const sorted = this.sortConfPatches(extconfPatches);
|
|
||||||
return sorted.findIndex(x => this.compareExtensionVersions(x.forVersion, version) > 0);
|
|
||||||
}
|
|
||||||
applySettingsPatches(oldVersion, patches) {
|
|
||||||
let index = this.findFirstNecessaryPatch(oldVersion, patches);
|
|
||||||
if (index === -1) {
|
|
||||||
this.logger?.log('info','settings','[Settings::applySettingsPatches] There are no pending conf patches.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// apply all remaining patches
|
|
||||||
this.logger?.log('info', 'settings', `[Settings::applySettingsPatches] There are ${patches.length - index} settings patches to apply`);
|
|
||||||
while (index < patches.length) {
|
|
||||||
const updateFn = patches[index].updateFn;
|
|
||||||
delete patches[index].forVersion;
|
|
||||||
delete patches[index].updateFn;
|
|
||||||
|
|
||||||
if (Object.keys(patches[index]).length > 0) {
|
|
||||||
ObjectCopy.overwrite(this.active, patches[index]);
|
|
||||||
}
|
|
||||||
if (updateFn) {
|
|
||||||
|
|
||||||
try {
|
|
||||||
updateFn(this.active, this.getDefaultSettings());
|
|
||||||
} catch (e) {
|
|
||||||
this.logger?.log('error', 'settings', '[Settings::applySettingsPatches] Failed to execute update function. Keeping settings object as-is. Error:', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
index++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async init() {
|
|
||||||
const settings = await this.get();
|
|
||||||
this.version = this.getExtensionVersion();
|
|
||||||
|
|
||||||
// |—> on first setup, settings is undefined & settings.version is haram
|
|
||||||
// | since new installs ship with updates by default, no patching is
|
|
||||||
// | needed. In this case, we assume we're on the current version
|
|
||||||
const oldVersion = (settings && settings.version) || this.version;
|
|
||||||
|
|
||||||
if (settings) {
|
|
||||||
this.logger?.log('info', 'settings', "[Settings::init] Configuration fetched from storage:", settings,
|
|
||||||
"\nlast saved with:", settings.version,
|
|
||||||
"\ncurrent version:", this.version
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// if (Debug.flushStoredSettings) {
|
|
||||||
// this.logger?.log('info', 'settings', "%c[Settings::init] Debug.flushStoredSettings is true. Using default settings", "background: #d00; color: #ffd");
|
|
||||||
// Debug.flushStoredSettings = false; // don't do it again this session
|
|
||||||
// this.active = this.getDefaultSettings();
|
|
||||||
// this.active.version = this.version;
|
|
||||||
// this.set(this.active);
|
|
||||||
// return this.active;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if there's no settings saved, return default settings.
|
|
||||||
if(! settings || (Object.keys(settings).length === 0 && settings.constructor === Object)) {
|
|
||||||
this.logger?.log(
|
|
||||||
'info',
|
|
||||||
'settings',
|
|
||||||
'[Settings::init] settings don\'t exist. Using defaults.\n#keys:',
|
|
||||||
settings ? Object.keys(settings).length : 0,
|
|
||||||
'\nsettings:',
|
|
||||||
settings
|
|
||||||
);
|
|
||||||
this.active = this.getDefaultSettings();
|
|
||||||
this.active.version = this.version;
|
|
||||||
await this.save();
|
|
||||||
return this.active;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if there's settings, set saved object as active settings
|
|
||||||
this.active = settings;
|
|
||||||
|
|
||||||
// if version number is undefined, we make it defined
|
|
||||||
// this should only happen on first extension initialization
|
|
||||||
if (!this.active.version) {
|
|
||||||
this.active.version = this.version;
|
|
||||||
await this.save();
|
|
||||||
return this.active;
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if extension has been updated. If not, return settings as they were retrieved
|
|
||||||
if (this.active.version === this.version) {
|
|
||||||
this.logger?.log('info', 'settings', "[Settings::init] extension was saved with current version of ultrawidify. Returning object as-is.");
|
|
||||||
return this.active;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This means extension update happened.
|
|
||||||
// btw fun fact — we can do version rollbacks, which might come in handy while testing
|
|
||||||
this.active.version = this.version;
|
|
||||||
|
|
||||||
// if extension has been updated, update existing settings with any options added in the
|
|
||||||
// new version. In addition to that, we remove old keys that are no longer used.
|
|
||||||
const patched = ObjectCopy.addNew(settings, this.default);
|
|
||||||
this.logger?.log('info', 'settings',"[Settings.init] Results from ObjectCopy.addNew()?", patched, "\n\nSettings from storage", settings, "\ndefault?", this.default);
|
|
||||||
|
|
||||||
if (patched) {
|
|
||||||
this.active = patched;
|
|
||||||
}
|
|
||||||
|
|
||||||
// in case settings in previous version contained a fucky wucky, we overwrite existing settings with a patch
|
|
||||||
this.applySettingsPatches(oldVersion, ExtensionConfPatch);
|
|
||||||
|
|
||||||
// set 'whatsNewChecked' flag to false when updating, always
|
|
||||||
this.active.whatsNewChecked = false;
|
|
||||||
// update settings version to current
|
|
||||||
this.active.version = this.version;
|
|
||||||
|
|
||||||
await this.save();
|
|
||||||
return this.active;
|
|
||||||
}
|
|
||||||
|
|
||||||
async get() {
|
|
||||||
let ret;
|
|
||||||
|
|
||||||
ret = await browser.storage.local.get('uwSettings');
|
|
||||||
|
|
||||||
this.logger?.log('info', 'settings', 'Got settings:', ret && ret.uwSettings && JSON.parse(ret.uwSettings));
|
|
||||||
|
|
||||||
try {
|
|
||||||
return JSON.parse(ret.uwSettings);
|
|
||||||
} catch(e) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fixSitesSettings(sites) {
|
|
||||||
for (const site in sites) {
|
|
||||||
if (site === '@global') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (sites[site].mode === undefined) {
|
|
||||||
sites[site].mode = ExtensionMode.Default;
|
|
||||||
}
|
|
||||||
if (sites[site].autoar === undefined) {
|
|
||||||
sites[site].autoar = ExtensionMode.Default;
|
|
||||||
}
|
|
||||||
if (sites[site].stretch === undefined) {
|
|
||||||
sites[site].stretch = StretchType.Default;
|
|
||||||
}
|
|
||||||
if (sites[site].videoAlignment === undefined) {
|
|
||||||
sites[site].videoAlignment = VideoAlignmentType.Default;
|
|
||||||
}
|
|
||||||
if (sites[site].keyboardShortcutsEnabled === undefined) {
|
|
||||||
sites[site].keyboardShortcutsEnabled = ExtensionMode.Default;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async set(extensionConf, options?) {
|
|
||||||
if (!options || !options.forcePreserveVersion) {
|
|
||||||
extensionConf.version = this.version;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.fixSitesSettings(extensionConf.sites);
|
|
||||||
|
|
||||||
this.logger?.log('info', 'settings', "[Settings::set] setting new settings:", extensionConf)
|
|
||||||
|
|
||||||
return browser.storage.local.set( {'uwSettings': JSON.stringify(extensionConf)});
|
|
||||||
}
|
|
||||||
|
|
||||||
async setActive(activeSettings) {
|
|
||||||
this.active = activeSettings;
|
|
||||||
}
|
|
||||||
|
|
||||||
async setProp(prop, value) {
|
|
||||||
this.active[prop] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
async save(options?) {
|
|
||||||
if (Debug.debug && Debug.storage) {
|
|
||||||
console.log("[Settings::save] Saving active settings:", this.active);
|
|
||||||
}
|
|
||||||
this.active.preventReload = undefined;
|
|
||||||
await this.set(this.active, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async saveWithoutReload() {
|
|
||||||
this.active.preventReload = true;
|
|
||||||
await this.set(this.active);
|
|
||||||
}
|
|
||||||
|
|
||||||
async rollback() {
|
|
||||||
this.active = await this.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
getDefaultSettings() {
|
|
||||||
return JSON.parse(JSON.stringify(this.default));
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----------------------------------------
|
|
||||||
// Nastavitve za posamezno stran
|
|
||||||
// Config for a given page:
|
|
||||||
//
|
|
||||||
// <hostname> : {
|
|
||||||
// status: <option> // should extension work on this site?
|
|
||||||
// arStatus: <option> // should we do autodetection on this site?
|
|
||||||
// statusEmbedded: <option> // reserved for future... maybe
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// Veljavne vrednosti za možnosti
|
|
||||||
// Valid values for options:
|
|
||||||
//
|
|
||||||
// status, arStatus, statusEmbedded:
|
|
||||||
//
|
|
||||||
// * enabled — always allow
|
|
||||||
// * basic — only allow fullscreen
|
|
||||||
// * default — allow if default is to allow, block if default is to block
|
|
||||||
// * disabled — never allow
|
|
||||||
|
|
||||||
|
|
||||||
getActionsForSite(site) {
|
|
||||||
if (!site) {
|
|
||||||
return this.active.actions;
|
|
||||||
}
|
|
||||||
if (this.active.sites[site] && this.active.sites[site].actions && this.active.sites[site].actions.length > 0) {
|
|
||||||
return this.active.sites[site].actions;
|
|
||||||
}
|
|
||||||
return this.active.actions;
|
|
||||||
}
|
|
||||||
|
|
||||||
getSettingsForSite(site?) {
|
|
||||||
if (!site) {
|
|
||||||
site = window.location.hostname;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.active.sites[site];
|
|
||||||
}
|
|
||||||
|
|
||||||
getExtensionMode(site?: string) {
|
|
||||||
if (!site) {
|
|
||||||
site = window.location.hostname;
|
|
||||||
|
|
||||||
if (!site) {
|
|
||||||
this.logger?.log('info', 'settings', `[Settings::canStartExtension] window.location.hostname is null or undefined: ${window.location.hostname} \nactive settings:`, this.active);
|
|
||||||
return ExtensionMode.Disabled;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// if site-specific settings don't exist for the site, we use default mode:
|
|
||||||
if (! this.active.sites[site]) {
|
|
||||||
return this.getExtensionMode('@global');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.active.sites[site].mode === ExtensionMode.Enabled) {
|
|
||||||
return ExtensionMode.Enabled;
|
|
||||||
} else if (this.active.sites[site].mode === ExtensionMode.Basic) {
|
|
||||||
return ExtensionMode.Basic;
|
|
||||||
} else if (this.active.sites[site].mode === ExtensionMode.Disabled) {
|
|
||||||
return ExtensionMode.Disabled;
|
|
||||||
} else {
|
|
||||||
if (site !== '@global') {
|
|
||||||
return this.getExtensionMode('@global');
|
|
||||||
} else {
|
|
||||||
return ExtensionMode.Disabled;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch(e){
|
|
||||||
this.logger?.log('error', 'settings', "[Settings.js::canStartExtension] Something went wrong — are settings defined/has init() been called?\n\nerror:", e, "\n\nSettings object:", this)
|
|
||||||
|
|
||||||
return ExtensionMode.Disabled;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
canStartExtension(site) {
|
|
||||||
// returns 'true' if extension can be started on a given site. Returns false if we shouldn't run.
|
|
||||||
if (!site) {
|
|
||||||
site = window.location.hostname;
|
|
||||||
|
|
||||||
if (!site) {
|
|
||||||
this.logger?.log('info', 'settings', `[Settings::canStartExtension] window.location.hostname is null or undefined: ${window.location.hostname} \nactive settings:`, this.active);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// if (Debug.debug) {
|
|
||||||
// // let's just temporarily disable debugging while recursively calling
|
|
||||||
// // this function to get extension status on current site without duplo
|
|
||||||
// // console logs (and without endless recursion)
|
|
||||||
// Debug.debug = false;
|
|
||||||
// const cse = this.canStartExtension(site);
|
|
||||||
// Debug.debug = true;
|
|
||||||
// }
|
|
||||||
try{
|
|
||||||
// if site is not defined, we use default mode:
|
|
||||||
if (! this.active.sites[site] || this.active.sites[site].mode === ExtensionMode.Default) {
|
|
||||||
return this.active.sites['@global'].mode === ExtensionMode.Enabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.active.sites['@global'].mode === ExtensionMode.Enabled) {
|
|
||||||
return this.active.sites[site].mode !== ExtensionMode.Disabled;
|
|
||||||
} else if (this.active.sites['@global'].mode === ExtensionMode.Whitelist) {
|
|
||||||
return this.active.sites[site].mode === ExtensionMode.Enabled;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} catch(e) {
|
|
||||||
this.logger?.log('error', 'settings', "[Settings.js::canStartExtension] Something went wrong — are settings defined/has init() been called?\nSettings object:", this);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
keyboardShortcutsEnabled(site) {
|
|
||||||
if (!site) {
|
|
||||||
site = window.location.hostname;
|
|
||||||
}
|
|
||||||
if (!site) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (!this.active.sites[site]
|
|
||||||
|| this.active.sites[site].keyboardShortcutsEnabled === undefined
|
|
||||||
|| this.active.sites[site].keyboardShortcutsEnabled === ExtensionMode.Default) {
|
|
||||||
return this.keyboardShortcutsEnabled('@global');
|
|
||||||
} else {
|
|
||||||
return this.active.sites[site].keyboardShortcutsEnabled === ExtensionMode.Enabled;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
this.logger?.log('info', 'settings',"[Settings.js::keyboardDisabled] something went wrong:", e);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extensionEnabled(){
|
|
||||||
return this.active.sites['@global'].mode !== ExtensionMode.Disabled
|
|
||||||
}
|
|
||||||
|
|
||||||
extensionEnabledForSite(site) {
|
|
||||||
return this.canStartExtension(site);
|
|
||||||
}
|
|
||||||
|
|
||||||
canStartAutoAr(site?: string) {
|
|
||||||
// 'site' argument is only ever used when calling this function recursively for debugging
|
|
||||||
if (!site) {
|
|
||||||
site = window.location.hostname;
|
|
||||||
|
|
||||||
if (!site) {
|
|
||||||
this.logger?.log('warn', ['settings', 'init', 'debug'], `[Settings::canStartAutoAr] No site — even window.location.hostname returned nothing!: ${window.location.hostname}`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// if (Debug.debug) {
|
|
||||||
// let's just temporarily disable debugging while recursively calling
|
|
||||||
// this function to get extension status on current site without duplo
|
|
||||||
// console logs (and without endless recursion)
|
|
||||||
// Debug.debug = false;
|
|
||||||
// const csar = this.canStartAutoAr(site);
|
|
||||||
// Debug.debug = true;
|
|
||||||
|
|
||||||
this.logger?.log('info', ['settings', 'init', 'debug'], "[Settings::canStartAutoAr] ----------------\nCAN WE START AUTOAR ON SITE", site,
|
|
||||||
"?\n\nsettings.active.sites[site]=", this.active.sites[site], "settings.active.sites[@global]=", this.active.sites['@global'],
|
|
||||||
"\nAutoar mode (global)?", this.active.sites['@global'].autoar,
|
|
||||||
`\nAutoar mode (${site})`, this.active.sites[site] ? this.active.sites[site].autoar : '<not defined>',
|
|
||||||
// "\nCan autoar be started?", csar
|
|
||||||
);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if site is not defined, we use default mode:
|
|
||||||
if (! this.active.sites[site]) {
|
|
||||||
this.logger?.log('info', ['settings', 'aard', 'init', 'debug'], "[Settings::canStartAutoAr] Settings not defined for this site, returning defaults.", site, this.active.sites[site], this.active.sites);
|
|
||||||
return this.active.sites['@global'].autoar === ExtensionMode.Enabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.active.sites['@global'].autoar === ExtensionMode.Enabled) {
|
|
||||||
this.logger?.log('info', ['settings', 'aard', 'init', 'debug'], `[Settings::canStartAutoAr] Aard is enabled by default. Extension can run unless disabled for this site.`, this.active.sites[site].autoar);
|
|
||||||
return this.active.sites[site].autoar !== ExtensionMode.Disabled;
|
|
||||||
} else if (this.active.sites['@global'].autoar === ExtensionMode.Whitelist) {
|
|
||||||
this.logger?.log('info', ['settings', 'init', 'debug'], "canStartAutoAr — can(not) start aard because extension is in whitelist mode, and this site is (not) equal to", ExtensionMode.Enabled)
|
|
||||||
return this.active.sites[site].autoar === ExtensionMode.Enabled;
|
|
||||||
} else {
|
|
||||||
this.logger?.log('info', ['settings', 'init', 'debug'], "canStartAutoAr — cannot start aard because extension is globally disabled")
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getDefaultOption(option?) {
|
|
||||||
const allDefault = {
|
|
||||||
mode: ExtensionMode.Default,
|
|
||||||
autoar: ExtensionMode.Default,
|
|
||||||
autoarFallback: ExtensionMode.Default,
|
|
||||||
stretch: StretchType.Default,
|
|
||||||
videoAlignment: VideoAlignmentType.Default,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!option || allDefault[option] === undefined) {
|
|
||||||
return allDefault;
|
|
||||||
}
|
|
||||||
|
|
||||||
return allDefault[option];
|
|
||||||
}
|
|
||||||
|
|
||||||
getDefaultAr(site) {
|
|
||||||
// site = this.getSiteSettings(site);
|
|
||||||
|
|
||||||
// if (site.defaultAr) {
|
|
||||||
// return site.defaultAr;
|
|
||||||
// }
|
|
||||||
return this.active.miscSettings.defaultAr;
|
|
||||||
}
|
|
||||||
|
|
||||||
getDefaultStretchMode(site) {
|
|
||||||
if (site && (this.active.sites[site]?.stretch ?? StretchType.Default) !== StretchType.Default) {
|
|
||||||
return this.active.sites[site].stretch;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.active.sites['@global'].stretch;
|
|
||||||
}
|
|
||||||
|
|
||||||
getDefaultCropPersistenceMode(site) {
|
|
||||||
if (site && (this.active.sites[site]?.cropModePersistence ?? StretchType.Default) !== StretchType.Default) {
|
|
||||||
return this.active.sites[site].cropModePersistence;
|
|
||||||
}
|
|
||||||
|
|
||||||
// persistence mode thing is missing from settings by default
|
|
||||||
return this.active.sites['@global'].cropModePersistence || CropModePersistence.Disabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
getDefaultVideoAlignment(site) {
|
|
||||||
if ( (this.active.sites[site]?.videoAlignment ?? VideoAlignmentType.Default) !== VideoAlignmentType.Default) {
|
|
||||||
return this.active.sites[site].videoAlignment;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.active.sites['@global'].videoAlignment;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Settings;
|
|
||||||
|
|
||||||
if(process.env.CHANNEL !== 'stable'){
|
|
||||||
console.info("Settings loaded");
|
|
||||||
}
|
|
||||||
@ -1,973 +0,0 @@
|
|||||||
|
|
||||||
import Debug from '../../conf/Debug';
|
|
||||||
import EdgeDetect from './edge-detect/EdgeDetect';
|
|
||||||
import EdgeStatus from './edge-detect/enums/EdgeStatusEnum';
|
|
||||||
import EdgeDetectPrimaryDirection from './edge-detect/enums/EdgeDetectPrimaryDirectionEnum';
|
|
||||||
import EdgeDetectQuality from './edge-detect/enums/EdgeDetectQualityEnum';
|
|
||||||
import GuardLine from './GuardLine';
|
|
||||||
// import DebugCanvas from './DebugCanvas';
|
|
||||||
import VideoAlignmentType from '../../../common/enums/VideoAlignmentType.enum';
|
|
||||||
import AspectRatioType from '../../../common/enums/AspectRatioType.enum';
|
|
||||||
import {sleep} from '../Util';
|
|
||||||
import BrowserDetect from '../../conf/BrowserDetect';
|
|
||||||
import Logger from '../Logger';
|
|
||||||
import VideoData from '../video-data/VideoData';
|
|
||||||
import Settings from '../Settings';
|
|
||||||
|
|
||||||
class ArDetector {
|
|
||||||
logger: Logger;
|
|
||||||
conf: VideoData;
|
|
||||||
video: HTMLVideoElement;
|
|
||||||
settings: Settings;
|
|
||||||
|
|
||||||
guardLine: GuardLine;
|
|
||||||
edgeDetector: EdgeDetect;
|
|
||||||
|
|
||||||
setupTimer: any;
|
|
||||||
sampleCols: any[];
|
|
||||||
sampleLines
|
|
||||||
|
|
||||||
canFallback: boolean = true;
|
|
||||||
fallbackMode: boolean = false;
|
|
||||||
|
|
||||||
blackLevel: number;
|
|
||||||
|
|
||||||
arid: string;
|
|
||||||
|
|
||||||
// ar detector starts in this state. running main() sets both to false
|
|
||||||
_paused: boolean;
|
|
||||||
_halted: boolean = true;
|
|
||||||
_exited: boolean = true;
|
|
||||||
|
|
||||||
private manualTickEnabled: boolean;
|
|
||||||
_nextTick: boolean;
|
|
||||||
canDoFallbackMode: boolean = false;
|
|
||||||
|
|
||||||
// helper objects
|
|
||||||
private attachedCanvas: HTMLCanvasElement;
|
|
||||||
canvas: HTMLCanvasElement;
|
|
||||||
private blackframeCanvas: HTMLCanvasElement;
|
|
||||||
private context: CanvasRenderingContext2D;
|
|
||||||
private blackframeContext: CanvasRenderingContext2D;
|
|
||||||
private canvasScaleFactor: number;
|
|
||||||
private detectionTimeoutEventCount: number;
|
|
||||||
canvasImageDataRowLength: number;
|
|
||||||
private noLetterboxCanvasReset: boolean;
|
|
||||||
private detectedAr: any;
|
|
||||||
private canvasDrawWindowHOffset: number;
|
|
||||||
private sampleCols_current: number;
|
|
||||||
|
|
||||||
constructor(videoData){
|
|
||||||
this.logger = videoData.logger;
|
|
||||||
this.conf = videoData;
|
|
||||||
this.video = videoData.video;
|
|
||||||
this.settings = videoData.settings;
|
|
||||||
|
|
||||||
this.sampleCols = [];
|
|
||||||
|
|
||||||
this.blackLevel = this.settings.active.arDetect.blackbar.blackLevel;
|
|
||||||
|
|
||||||
this.arid = (Math.random()*100).toFixed();
|
|
||||||
|
|
||||||
// we can tick manually, for debugging
|
|
||||||
this.logger.log('info', 'init', `[ArDetector::ctor] creating new ArDetector. arid: ${this.arid}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
setManualTick(manualTick) {
|
|
||||||
this.manualTickEnabled = manualTick;
|
|
||||||
}
|
|
||||||
|
|
||||||
tick() {
|
|
||||||
this._nextTick = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
init(){
|
|
||||||
this.logger.log('info', 'init', `[ArDetect::init] <@${this.arid}> Initializing autodetection.`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (this.settings.canStartAutoAr()) {
|
|
||||||
this.setup();
|
|
||||||
} else {
|
|
||||||
throw "Settings prevent autoar from starting"
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
this.logger.log('error', 'init', `%c[ArDetect::init] <@${this.arid}> Initialization failed.`, _ard_console_stop, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
destroy(){
|
|
||||||
this.logger.log('info', 'init', `%c[ArDetect::destroy] <@${this.arid}> Destroying aard.`, _ard_console_stop);
|
|
||||||
// this.debugCanvas.destroy();
|
|
||||||
this.stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
setup(cwidth?: number, cheight?: number){
|
|
||||||
this.logger.log('info', 'init', `[ArDetect::setup] <@${this.arid}> Starting autodetection setup.`);
|
|
||||||
//
|
|
||||||
// [-1] check for zero-width and zero-height videos. If we detect this, we kick the proverbial
|
|
||||||
// can some distance down the road. This problem will prolly fix itself soon. We'll also
|
|
||||||
// not do any other setup until this issue is fixed
|
|
||||||
//
|
|
||||||
if (this.video.videoWidth === 0 || this.video.videoHeight === 0 ){
|
|
||||||
this.logger.log('warn', 'debug', `[ArDetect::setup] <@${this.arid}> This video has zero width or zero height. Dimensions: ${this.video.videoWidth} × ${this.video.videoHeight}`);
|
|
||||||
|
|
||||||
this.scheduleInitRestart();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// [0] initiate "dependencies" first
|
|
||||||
//
|
|
||||||
|
|
||||||
this.guardLine = new GuardLine(this);
|
|
||||||
this.edgeDetector = new EdgeDetect(this);
|
|
||||||
// this.debugCanvas = new DebugCanvas(this);
|
|
||||||
|
|
||||||
|
|
||||||
//
|
|
||||||
// [1] initiate canvases
|
|
||||||
//
|
|
||||||
|
|
||||||
if (!cwidth) {
|
|
||||||
cwidth = this.settings.active.arDetect.canvasDimensions.sampleCanvas.width;
|
|
||||||
cheight = this.settings.active.arDetect.canvasDimensions.sampleCanvas.height;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.canvas) {
|
|
||||||
this.canvas.remove();
|
|
||||||
}
|
|
||||||
if (this.blackframeCanvas) {
|
|
||||||
this.blackframeCanvas.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
// things to note: we'll be keeping canvas in memory only.
|
|
||||||
this.canvas = document.createElement("canvas");
|
|
||||||
this.canvas.width = cwidth;
|
|
||||||
this.canvas.height = cheight;
|
|
||||||
this.blackframeCanvas = document.createElement("canvas");
|
|
||||||
this.blackframeCanvas.width = this.settings.active.arDetect.canvasDimensions.blackframeCanvas.width;
|
|
||||||
this.blackframeCanvas.height = this.settings.active.arDetect.canvasDimensions.blackframeCanvas.height;
|
|
||||||
|
|
||||||
this.context = this.canvas.getContext("2d");
|
|
||||||
this.blackframeContext = this.blackframeCanvas.getContext("2d");
|
|
||||||
|
|
||||||
|
|
||||||
// do setup once
|
|
||||||
// tho we could do it for every frame
|
|
||||||
this.canvasScaleFactor = cheight / this.video.videoHeight;
|
|
||||||
|
|
||||||
|
|
||||||
//
|
|
||||||
// [2] determine places we'll use to sample our main frame
|
|
||||||
//
|
|
||||||
|
|
||||||
let ncol = this.settings.active.arDetect.sampling.staticCols;
|
|
||||||
let nrow = this.settings.active.arDetect.sampling.staticRows;
|
|
||||||
|
|
||||||
let colSpacing = this.canvas.width / ncol;
|
|
||||||
let rowSpacing = (this.canvas.height << 2) / nrow;
|
|
||||||
|
|
||||||
this.sampleLines = [];
|
|
||||||
this.sampleCols = [];
|
|
||||||
|
|
||||||
for(let i = 0; i < ncol; i++){
|
|
||||||
if(i < ncol - 1)
|
|
||||||
this.sampleCols.push(Math.round(colSpacing * i));
|
|
||||||
else{
|
|
||||||
this.sampleCols.push(Math.round(colSpacing * i) - 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for(let i = 0; i < nrow; i++){
|
|
||||||
if(i < ncol - 5)
|
|
||||||
this.sampleLines.push(Math.round(rowSpacing * i));
|
|
||||||
else{
|
|
||||||
this.sampleLines.push(Math.round(rowSpacing * i) - 4);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// [3] detect if we're in the fallback mode and reset guardline
|
|
||||||
//
|
|
||||||
|
|
||||||
if (this.fallbackMode) {
|
|
||||||
this.logger.log('warn', 'debug', `[ArDetect::setup] <@${this.arid}> WARNING: CANVAS RESET DETECTED/we're in fallback mode - recalculating guardLine`, "background: #000; color: #ff2");
|
|
||||||
// blackbar, imagebar
|
|
||||||
this.guardLine.reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// [4] see if browser supports "fallback mode" by drawing a small portion of our window
|
|
||||||
//
|
|
||||||
|
|
||||||
try {
|
|
||||||
(this.blackframeContext as any).drawWindow(window,0, 0, this.blackframeCanvas.width, this.blackframeCanvas.height, "rgba(0,0,128,1)");
|
|
||||||
this.canDoFallbackMode = true;
|
|
||||||
} catch (e) {
|
|
||||||
this.canDoFallbackMode = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// [5] do other things setup needs to do
|
|
||||||
//
|
|
||||||
|
|
||||||
this.detectionTimeoutEventCount = 0;
|
|
||||||
this.resetBlackLevel();
|
|
||||||
|
|
||||||
// if we're restarting ArDetect, we need to do this in order to force-recalculate aspect ratio
|
|
||||||
this.conf.resizer.setLastAr({type: AspectRatioType.Automatic, ratio: this.getDefaultAr()});
|
|
||||||
|
|
||||||
this.canvasImageDataRowLength = cwidth << 2;
|
|
||||||
this.noLetterboxCanvasReset = false;
|
|
||||||
|
|
||||||
if (this.settings.canStartAutoAr() ) {
|
|
||||||
this.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
if(Debug.debugCanvas.enabled){
|
|
||||||
// this.debugCanvas.init({width: cwidth, height: cheight});
|
|
||||||
// DebugCanvas.draw("test marker","test","rect", {x:5, y:5}, {width: 5, height: 5});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.conf.arSetupComplete = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
start() {
|
|
||||||
if (this.settings.canStartAutoAr()) {
|
|
||||||
this.logger.log('info', 'debug', `"%c[ArDetect::start] <@${this.arid}> Starting automatic aspect ratio detection`, _ard_console_start);
|
|
||||||
} else {
|
|
||||||
this.logger.log('warn', 'debug', `"%c[ArDetect::start] <@${this.arid}> Wanted to start automatic aspect ratio detection, but settings don't allow that. Aard won't be started.`, _ard_console_change);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.conf.resizer.lastAr.type === AspectRatioType.Automatic) {
|
|
||||||
// ensure first autodetection will run in any case
|
|
||||||
this.conf.resizer.setLastAr({type: AspectRatioType.Automatic, ratio: this.getDefaultAr()});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// launch main() if it's currently not running:
|
|
||||||
this.main();
|
|
||||||
// automatic detection starts halted. If halted=false when main first starts, extension won't run
|
|
||||||
// this._paused is undefined the first time we run this function, which is effectively the same thing
|
|
||||||
// as false. Still, we'll explicitly fix this here.
|
|
||||||
this._paused = false;
|
|
||||||
this._halted = false;
|
|
||||||
this._paused = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
unpause() {
|
|
||||||
// pause only if we were running before. Don't pause if we aren't running
|
|
||||||
// (we are running when _halted is neither true nor undefined)
|
|
||||||
if (this._paused && this._halted === false) {
|
|
||||||
this._paused = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pause() {
|
|
||||||
// pause only if we were running before. Don't pause if we aren't running
|
|
||||||
// (we are running when _halted is neither true nor undefined)
|
|
||||||
if (this._halted === false) {
|
|
||||||
this._paused = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stop(){
|
|
||||||
this.logger.log('info', 'debug', `"%c[ArDetect::stop] <@${this.arid}> Stopping automatic aspect ratio detection`, _ard_console_stop);
|
|
||||||
this._halted = true;
|
|
||||||
// this.conf.resizer.setArLastAr();
|
|
||||||
}
|
|
||||||
|
|
||||||
async main() {
|
|
||||||
if (this._paused) {
|
|
||||||
// unpause if paused
|
|
||||||
this._paused = false;
|
|
||||||
return; // main loop still keeps executing. Return is needed to avoid a million instances of autodetection
|
|
||||||
}
|
|
||||||
if (!this._halted) {
|
|
||||||
// we are already running, don't run twice
|
|
||||||
// this would have handled the 'paused' from before, actually.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let exitedRetries = 10;
|
|
||||||
|
|
||||||
while (!this._exited && exitedRetries --> 0) {
|
|
||||||
this.logger.log('warn', 'debug', `[ArDetect::main] <@${this.arid}> We are trying to start another instance of autodetection on current video, but the previous instance hasn't exited yet. Waiting for old instance to exit ...`);
|
|
||||||
await sleep(this.settings.active.arDetect.timers.tickrate);
|
|
||||||
}
|
|
||||||
if (!this._exited) {
|
|
||||||
this.logger.log('error', 'debug', `[ArDetect::main] <@${this.arid}> Previous instance didn't exit in time. Not starting a new one.`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.log('info', 'debug', `%c[ArDetect::main] <@${this.arid}> Previous instance didn't exit in time. Not starting a new one.`);
|
|
||||||
|
|
||||||
// we need to unhalt:
|
|
||||||
this._halted = false;
|
|
||||||
this._exited = false;
|
|
||||||
|
|
||||||
// set initial timestamps so frame check will trigger the first time we run the loop
|
|
||||||
let lastFrameCheckStartTime = Date.now() - (this.settings.active.arDetect.timers.playing << 1);
|
|
||||||
|
|
||||||
const frameCheckTimes = new Array(10).fill(-1);
|
|
||||||
let frameCheckBufferIndex = 0;
|
|
||||||
let fcstart, fctime;
|
|
||||||
|
|
||||||
while (this && !this._halted) {
|
|
||||||
// NOTE: we separated tickrate and inter-check timeouts so that when video switches
|
|
||||||
// state from 'paused' to 'playing', we don't need to wait for the rest of the longer
|
|
||||||
// paused state timeout to finish.
|
|
||||||
|
|
||||||
if ( (!this.manualTickEnabled && this.canTriggerFrameCheck(lastFrameCheckStartTime)) || this._nextTick) {
|
|
||||||
this._nextTick = false;
|
|
||||||
|
|
||||||
lastFrameCheckStartTime = Date.now();
|
|
||||||
fcstart = performance.now();
|
|
||||||
|
|
||||||
try {
|
|
||||||
this.frameCheck();
|
|
||||||
} catch (e) {
|
|
||||||
this.logger.log('error', 'debug', `%c[ArDetect::main] <@${this.arid}> Frame check failed:`, "color: #000, background: #f00", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Debug.performanceMetrics) {
|
|
||||||
fctime = performance.now() - fcstart;
|
|
||||||
frameCheckTimes[frameCheckBufferIndex % frameCheckTimes.length] = fctime;
|
|
||||||
this.conf.pageInfo.sendPerformanceUpdate({frameCheckTimes: frameCheckTimes, lastFrameCheckTime: fctime});
|
|
||||||
++frameCheckBufferIndex;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await sleep(this.settings.active.arDetect.timers.tickrate);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.log('info', 'debug', `%c[ArDetect::main] <@${this.arid}> Main autodetection loop exited. Halted? ${this._halted}`, _ard_console_stop);
|
|
||||||
this._exited = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
canTriggerFrameCheck(lastFrameCheckStartTime) {
|
|
||||||
if (this._paused) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (this.video.ended || this.video.paused){
|
|
||||||
// we slow down if ended or pausing. Detecting is pointless.
|
|
||||||
// we don't stop outright in case seeking happens during pause/after video was
|
|
||||||
// ended and video gets into 'playing' state again
|
|
||||||
return Date.now() - lastFrameCheckStartTime > this.settings.active.arDetect.timers.paused;
|
|
||||||
}
|
|
||||||
if (this.video.error){
|
|
||||||
// če je video pavziran, še vedno skušamo zaznati razmerje stranic - ampak bolj poredko.
|
|
||||||
// if the video is paused, we still do autodetection. We just do it less often.
|
|
||||||
return Date.now() - lastFrameCheckStartTime > this.settings.active.arDetect.timers.error;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Date.now() - lastFrameCheckStartTime > this.settings.active.arDetect.timers.playing;
|
|
||||||
}
|
|
||||||
|
|
||||||
isRunning(){
|
|
||||||
return ! (this._halted || this._paused || this._exited);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
scheduleInitRestart(timeout?: number, force_reset?: boolean){
|
|
||||||
if(! timeout){
|
|
||||||
timeout = 100;
|
|
||||||
}
|
|
||||||
// don't allow more than 1 instance
|
|
||||||
if(this.setupTimer){
|
|
||||||
clearTimeout(this.setupTimer);
|
|
||||||
}
|
|
||||||
|
|
||||||
let ths = this;
|
|
||||||
this.setupTimer = setTimeout(function(){
|
|
||||||
ths.setupTimer = null;
|
|
||||||
try{
|
|
||||||
ths.main();
|
|
||||||
} catch(e) {
|
|
||||||
this.logger('error', 'debug', `[ArDetector::scheduleInitRestart] <@${this.arid}> Failed to start main(). Error:`,e);
|
|
||||||
}
|
|
||||||
ths = null;
|
|
||||||
},
|
|
||||||
timeout
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//#region helper functions (general)
|
|
||||||
attachCanvas(canvas){
|
|
||||||
if(this.attachedCanvas)
|
|
||||||
this.attachedCanvas.remove();
|
|
||||||
|
|
||||||
// todo: place canvas on top of the video instead of random location
|
|
||||||
canvas.style.position = "absolute";
|
|
||||||
canvas.style.left = "200px";
|
|
||||||
canvas.style.top = "1200px";
|
|
||||||
canvas.style.zIndex = 10000;
|
|
||||||
|
|
||||||
document.getElementsByTagName("body")[0]
|
|
||||||
.appendChild(canvas);
|
|
||||||
}
|
|
||||||
|
|
||||||
canvasReadyForDrawWindow(){
|
|
||||||
this.logger.log('info', 'debug', `%c[ArDetect::canvasReadyForDrawWindow] <@${this.arid}> canvas is ${this.canvas.height === window.innerHeight ? '' : 'NOT '}ready for drawWindow(). Canvas height: ${this.canvas.height}px; window inner height: ${window.innerHeight}px.`)
|
|
||||||
|
|
||||||
return this.canvas.height == window.innerHeight
|
|
||||||
}
|
|
||||||
|
|
||||||
getTimeout(baseTimeout, startTime){
|
|
||||||
let execTime = (performance.now() - startTime);
|
|
||||||
|
|
||||||
return baseTimeout;
|
|
||||||
}
|
|
||||||
//#endregion
|
|
||||||
|
|
||||||
getDefaultAr() {
|
|
||||||
const ratio = this.video.videoWidth / this.video.videoHeight;
|
|
||||||
if (isNaN(ratio)) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return ratio;
|
|
||||||
}
|
|
||||||
|
|
||||||
calculateArFromEdges(edges) {
|
|
||||||
// if we don't specify these things, they'll have some default values.
|
|
||||||
if(edges.top === undefined){
|
|
||||||
edges.top = 0;
|
|
||||||
edges.bottom = 0;
|
|
||||||
edges.left = 0; // RESERVED FOR FUTURE — CURRENTLY UNUSED
|
|
||||||
edges.right = 0; // THIS FUNCTION CAN PRESENTLY ONLY HANDLE LETTERBOX
|
|
||||||
}
|
|
||||||
|
|
||||||
let letterbox = edges.top + edges.bottom;
|
|
||||||
|
|
||||||
|
|
||||||
if (! this.fallbackMode) {
|
|
||||||
// Since video is stretched to fit the canvas, we need to take that into account when calculating target
|
|
||||||
// aspect ratio and correct our calculations to account for that
|
|
||||||
|
|
||||||
const fileAr = this.video.videoWidth / this.video.videoHeight;
|
|
||||||
const canvasAr = this.canvas.width / this.canvas.height;
|
|
||||||
let widthCorrected;
|
|
||||||
|
|
||||||
if (edges.top && edges.bottom) {
|
|
||||||
// in case of letterbox, we take canvas height as canon and assume width got stretched or squished
|
|
||||||
|
|
||||||
if (fileAr != canvasAr) {
|
|
||||||
widthCorrected = this.canvas.height * fileAr;
|
|
||||||
} else {
|
|
||||||
widthCorrected = this.canvas.width;
|
|
||||||
}
|
|
||||||
|
|
||||||
return widthCorrected / (this.canvas.height - letterbox);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// fallback mode behaves a wee bit differently
|
|
||||||
|
|
||||||
let zoomFactor = 1;
|
|
||||||
|
|
||||||
// there's stuff missing from the canvas. We need to assume canvas' actual height is bigger by a factor x, where
|
|
||||||
// x = [video.zoomedHeight] / [video.unzoomedHeight]
|
|
||||||
//
|
|
||||||
// letterbox also needs to be corrected:
|
|
||||||
// letterbox += [video.zoomedHeight] - [video.unzoomedHeight]
|
|
||||||
|
|
||||||
let vbr = this.video.getBoundingClientRect();
|
|
||||||
|
|
||||||
zoomFactor = vbr.height / this.video.clientHeight;
|
|
||||||
letterbox += vbr.height - this.video.clientHeight;
|
|
||||||
|
|
||||||
let trueHeight = this.canvas.height * zoomFactor - letterbox;
|
|
||||||
|
|
||||||
if(edges.top > 1 && edges.top <= this.settings.active.arDetect.fallbackMode.noTriggerZonePx ){
|
|
||||||
this.logger.log('info', 'arDetect', `%c[ArDetect::calculateArFromEdges] <@${this.arid}> Edge is in the no-trigger zone. Aspect ratio change is not triggered.`)
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// varnostno območje, ki naj ostane črno (da lahko v fallback načinu odkrijemo ožanje razmerja stranic).
|
|
||||||
// x2, ker je safetyBorderPx definiran za eno stran.
|
|
||||||
// safety border so we can detect aspect ratio narrowing (21:9 -> 16:9).
|
|
||||||
// x2 because safetyBorderPx is for one side.
|
|
||||||
trueHeight += (this.settings.active.arDetect.fallbackMode.safetyBorderPx << 1);
|
|
||||||
|
|
||||||
return this.canvas.width * zoomFactor / trueHeight;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
processAr(trueAr){
|
|
||||||
this.detectedAr = trueAr;
|
|
||||||
|
|
||||||
// poglejmo, če se je razmerje stranic spremenilo
|
|
||||||
// check if aspect ratio is changed:
|
|
||||||
let lastAr = this.conf.resizer.getLastAr();
|
|
||||||
if (lastAr.type === AspectRatioType.Automatic && lastAr.ratio !== null && lastAr.ratio !== undefined){
|
|
||||||
// spremembo lahko zavrnemo samo, če uporabljamo avtomatski način delovanja in če smo razmerje stranic
|
|
||||||
// že nastavili.
|
|
||||||
//
|
|
||||||
// we can only deny aspect ratio changes if we use automatic mode and if aspect ratio was set from here.
|
|
||||||
|
|
||||||
let arDiff = trueAr - lastAr.ratio;
|
|
||||||
|
|
||||||
if (arDiff < 0)
|
|
||||||
arDiff = -arDiff;
|
|
||||||
|
|
||||||
const arDiff_percent = arDiff / trueAr;
|
|
||||||
|
|
||||||
// ali je sprememba v mejah dovoljenega? Če da -> fertik
|
|
||||||
// is ar variance within acceptable levels? If yes -> we done
|
|
||||||
this.logger.log('info', 'arDetect', `%c[ArDetect::processAr] <@${this.arid}> New aspect ratio varies from the old one by this much:\n`,"color: #aaf","old Ar", lastAr.ratio, "current ar", trueAr, "arDiff (absolute):",arDiff,"ar diff (relative to new ar)", arDiff_percent);
|
|
||||||
|
|
||||||
if (arDiff < trueAr * this.settings.active.arDetect.allowedArVariance){
|
|
||||||
this.logger.log('info', 'arDetect', `%c[ArDetect::processAr] <@${this.arid}> Aspect ratio change denied — diff %: ${arDiff_percent}`, "background: #740; color: #fa2");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.logger.log('info', 'arDetect', `%c[ArDetect::processAr] <@${this.arid}> aspect ratio change accepted — diff %: ${arDiff_percent}`, "background: #153; color: #4f9");
|
|
||||||
}
|
|
||||||
this.logger.log('info', 'debug', `%c[ArDetect::processAr] <@${this.arid}> Triggering aspect ratio change. New aspect ratio: ${trueAr}`, _ard_console_change);
|
|
||||||
|
|
||||||
this.conf.resizer.updateAr({type: AspectRatioType.Automatic, ratio: trueAr});
|
|
||||||
}
|
|
||||||
|
|
||||||
clearImageData(id) {
|
|
||||||
if ((ArrayBuffer as any).transfer) {
|
|
||||||
(ArrayBuffer as any).transfer(id, 0);
|
|
||||||
}
|
|
||||||
id = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
frameCheck(){
|
|
||||||
if(! this.video){
|
|
||||||
this.logger.log('error', 'debug', `%c[ArDetect::frameCheck] <@${this.arid}> Video went missing. Destroying current instance of videoData.`);
|
|
||||||
this.conf.destroy();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.blackframeContext) {
|
|
||||||
this.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
let startTime = performance.now();
|
|
||||||
let sampleCols = this.sampleCols.slice(0);
|
|
||||||
|
|
||||||
//
|
|
||||||
// [0] blackframe tests (they also determine whether we need fallback mode)
|
|
||||||
//
|
|
||||||
try {
|
|
||||||
this.blackframeContext.drawImage(this.video, 0, 0, this.blackframeCanvas.width, this.blackframeCanvas.height);
|
|
||||||
|
|
||||||
// special browsers require special tests
|
|
||||||
// if (this.hasDRM()) {
|
|
||||||
// this.fallbackMode = false;
|
|
||||||
// throw 'VIDEO_DRM_PROTECTED';
|
|
||||||
// }
|
|
||||||
this.fallbackMode = false;
|
|
||||||
} catch (e) {
|
|
||||||
this.logger.log('error', 'arDetect', `%c[ArDetect::frameCheck] <@${this.arid}> %c[ArDetect::frameCheck] can't draw image on canvas. ${this.canDoFallbackMode ? 'Trying canvas.drawWindow instead' : 'Doing nothing as browser doesn\'t support fallback mode.'}`, "color:#000; backgroud:#f51;", e);
|
|
||||||
|
|
||||||
// in case of DRM errors, we need to prevent the execution to reach the aspec
|
|
||||||
// ratio setting part of this function. For the time being, we're only stopping
|
|
||||||
// in case we encounter DRM error in Chrome. Firefox has fallback mode and generates
|
|
||||||
// different error, so that goes.
|
|
||||||
// if (e === 'VIDEO_DRM_PROTECTED') {
|
|
||||||
// // nothing to see here, really, if fallback mode isn't supported by browser
|
|
||||||
// if (!this.drmNotificationShown) {
|
|
||||||
// this.drmNotificationShown = true;
|
|
||||||
|
|
||||||
// this.conf.player.showNotification('AARD_DRM');
|
|
||||||
// this.conf.resizer.setAr({type: AspectRatio.Reset});
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
if (! this.canvasReadyForDrawWindow()) {
|
|
||||||
// this means canvas needs to be resized, so we'll just re-run setup with all those new parameters
|
|
||||||
this.stop();
|
|
||||||
|
|
||||||
let newCanvasWidth = window.innerHeight * (this.video.videoWidth / this.video.videoHeight);
|
|
||||||
let newCanvasHeight = window.innerHeight;
|
|
||||||
|
|
||||||
if (this.conf.resizer.videoAlignment === VideoAlignmentType.Center) {
|
|
||||||
this.canvasDrawWindowHOffset = Math.round((window.innerWidth - newCanvasWidth) * 0.5);
|
|
||||||
} else if (this.conf.resizer.videoAlignment === VideoAlignmentType.Left) {
|
|
||||||
this.canvasDrawWindowHOffset = 0;
|
|
||||||
} else {
|
|
||||||
this.canvasDrawWindowHOffset = window.innerWidth - newCanvasWidth;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setup(newCanvasWidth, newCanvasHeight);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// if this is the case, we'll first draw on canvas, as we'll need intermediate canvas if we want to get a
|
|
||||||
// smaller sample for blackframe check
|
|
||||||
this.fallbackMode = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
(this.context as any).drawWindow(window, this.canvasDrawWindowHOffset, 0, this.canvas.width, this.canvas.height, "rgba(0,0,128,1)");
|
|
||||||
} catch (e) {
|
|
||||||
this.logger.log('error', 'arDetect', `%c[ArDetect::frameCheck] can't draw image on canvas with fallback mode either. This error is prolly only temporary.`, "color:#000; backgroud:#f51;", e);
|
|
||||||
return; // it's prolly just a fluke, so we do nothing special here
|
|
||||||
}
|
|
||||||
// draw blackframe sample from our main sample:
|
|
||||||
this.blackframeContext.drawImage(this.canvas, this.blackframeCanvas.width, this.blackframeCanvas.height)
|
|
||||||
|
|
||||||
this.logger.log('info', 'arDetect_verbose', `%c[ArDetect::frameCheck] canvas.drawImage seems to have worked`, "color:#000; backgroud:#2f5;");
|
|
||||||
}
|
|
||||||
|
|
||||||
const bfanalysis = this.blackframeTest();
|
|
||||||
if (bfanalysis.isBlack) {
|
|
||||||
// we don't do any corrections on frames confirmed black
|
|
||||||
this.logger.log('info', 'arDetect_verbose', `%c[ArDetect::frameCheck] Black frame analysis suggests this frame is black or too dark. Doing nothing.`, "color: #fa3", bfanalysis);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// if we are in fallback mode, then frame has already been drawn to the main canvas.
|
|
||||||
// if we are in normal mode though, the frame has yet to be drawn
|
|
||||||
if (!this.fallbackMode) {
|
|
||||||
this.context.drawImage(this.video, 0, 0, this.canvas.width, this.canvas.height);
|
|
||||||
}
|
|
||||||
const imageData = this.context.getImageData(0, 0, this.canvas.width, this.canvas.height).data;
|
|
||||||
|
|
||||||
if (! this.fastLetterboxPresenceTest(imageData, sampleCols) ) {
|
|
||||||
// Če ne zaznamo letterboxa, kličemo reset. Lahko, da je bilo razmerje stranic popravljeno na roke. Možno je tudi,
|
|
||||||
// da je letterbox izginil.
|
|
||||||
// If we don't detect letterbox, we reset aspect ratio to aspect ratio of the video file. The aspect ratio could
|
|
||||||
// have been corrected manually. It's also possible that letterbox (that was there before) disappeared.
|
|
||||||
this.conf.resizer.updateAr({type: AspectRatioType.Automatic, ratio: this.getDefaultAr()});
|
|
||||||
this.guardLine.reset();
|
|
||||||
this.noLetterboxCanvasReset = true;
|
|
||||||
|
|
||||||
this.logger.log('info', 'arDetect_verbose', `%c[ArDetect::frameCheck] Letterbox not detected in fast test. Letterbox is either gone or we manually corrected aspect ratio. Nothing will be done.`, "color: #fa3");
|
|
||||||
|
|
||||||
this.clearImageData(imageData);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Če preverjamo naprej, potem moramo postaviti to vrednost nazaj na 'false'. V nasprotnem primeru se bo
|
|
||||||
// css resetiral enkrat na video/pageload namesto vsakič, ko so za nekaj časa obrobe odstranejene
|
|
||||||
// if we look further we need to reset this value back to false. Otherwise we'll only get CSS reset once
|
|
||||||
// per video/pageload instead of every time letterbox goes away (this can happen more than once per vid)
|
|
||||||
this.noLetterboxCanvasReset = false;
|
|
||||||
|
|
||||||
// poglejmo, če obrežemo preveč.
|
|
||||||
// let's check if we're cropping too much
|
|
||||||
const guardLineOut = this.guardLine.check(imageData, this.fallbackMode);
|
|
||||||
|
|
||||||
// če ni padla nobena izmed funkcij, potem se razmerje stranic ni spremenilo
|
|
||||||
// if both succeed, then aspect ratio hasn't changed.
|
|
||||||
if (!guardLineOut.imageFail && !guardLineOut.blackbarFail) {
|
|
||||||
this.logger.log('info', 'arDetect_verbose', `%c[ArDetect::frameCheck] guardLine tests were successful. (no imagefail and no blackbarfail)\n`, "color: #afa", guardLineOut);
|
|
||||||
this.clearImageData(imageData);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// drugače nadaljujemo, našemu vzorcu stolpcev pa dodamo tiste stolpce, ki so
|
|
||||||
// kršili blackbar (če obstajajo) ter jih razvrstimo
|
|
||||||
// otherwise we continue. We add blackbar violations to the list of the cols
|
|
||||||
// we'll sample and sort them
|
|
||||||
if (guardLineOut.blackbarFail) {
|
|
||||||
sampleCols.concat(guardLineOut.offenders).sort(
|
|
||||||
(a: number, b: number) => a - b
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// if we're in fallback mode and blackbar test failed, we restore CSS and quit
|
|
||||||
// (since the new letterbox edge isn't present in our sample due to technical
|
|
||||||
// limitations)
|
|
||||||
if (this.fallbackMode && guardLineOut.blackbarFail) {
|
|
||||||
this.conf.resizer.setAr({type: AspectRatioType.Automatic, ratio: this.getDefaultAr()});
|
|
||||||
this.guardLine.reset();
|
|
||||||
this.noLetterboxCanvasReset = true;
|
|
||||||
|
|
||||||
this.clearImageData(imageData);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// će se razmerje stranic spreminja iz ožjega na širšega, potem najprej poglejmo za prisotnostjo navpičnih črnih obrob.
|
|
||||||
// če so prisotne navpične obrobe tudi na levi in desni strani, potlej obstaja možnost, da gre za logo na črnem ozadju.
|
|
||||||
// v tem primeru obstaja nevarnost, da porežemo preveč. Ker obstaja dovolj velika možnost, da bi porezali preveč, rajši
|
|
||||||
// ne naredimo ničesar.
|
|
||||||
//
|
|
||||||
// če je pillarbox zaznan v primeru spremembe iz ožjega na širše razmerje stranice, razmerje povrnemo na privzeto vrednost.
|
|
||||||
//
|
|
||||||
// If aspect ratio changes from narrower to wider, we first check for presence of pillarbox. Presence of pillarbox indicates
|
|
||||||
// a chance of a logo on black background. We could cut easily cut too much. Because there's a somewhat significant chance
|
|
||||||
// that we will cut too much, we rather avoid doing anything at all. There's gonna be a next chance.
|
|
||||||
try{
|
|
||||||
if(guardLineOut.blackbarFail || guardLineOut.imageFail){
|
|
||||||
if(this.edgeDetector.findBars(imageData, null, EdgeDetectPrimaryDirection.Horizontal).status === 'ar_known'){
|
|
||||||
|
|
||||||
|
|
||||||
if(guardLineOut.blackbarFail){
|
|
||||||
this.logger.log('info', 'arDetect', `[ArDetect::frameCheck] Detected blackbar violation and pillarbox. Resetting to default aspect ratio.`);
|
|
||||||
this.conf.resizer.setAr({type: AspectRatioType.Automatic, ratio: this.getDefaultAr()});
|
|
||||||
this.guardLine.reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.clearImageData(imageData);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch(e) {
|
|
||||||
this.logger.log('info', 'arDetect', `[ArDetect::frameCheck] something went wrong while checking for pillarbox. Error:\n`, e);
|
|
||||||
}
|
|
||||||
|
|
||||||
// pa poglejmo, kje se končajo črne letvice na vrhu in na dnu videa.
|
|
||||||
// let's see where black bars end.
|
|
||||||
this.sampleCols_current = sampleCols.length;
|
|
||||||
|
|
||||||
// blackSamples -> {res_top, res_bottom}
|
|
||||||
|
|
||||||
let edgePost = this.edgeDetector.findBars(imageData, sampleCols, EdgeDetectPrimaryDirection.Vertical, EdgeDetectQuality.Improved, guardLineOut, bfanalysis);
|
|
||||||
|
|
||||||
this.logger.log('info', 'arDetect_verbose', `%c[ArDetect::frameCheck] edgeDetector returned this\n`, "color: #aaf", edgePost);
|
|
||||||
|
|
||||||
if (edgePost.status !== EdgeStatus.ARKnown){
|
|
||||||
// rob ni bil zaznan, zato ne naredimo ničesar.
|
|
||||||
// no edge was detected. Let's leave things as they were
|
|
||||||
this.logger.log('info', 'arDetect_verbose', `%c[ArDetect::frameCheck] Edge wasn't detected with findBars`, "color: #fa3", edgePost, "EdgeStatus.AR_KNOWN:", EdgeStatus.ARKnown);
|
|
||||||
|
|
||||||
this.clearImageData(imageData);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let newAr = this.calculateArFromEdges(edgePost);
|
|
||||||
|
|
||||||
this.logger.log('info', 'arDetect_verbose', `%c[ArDetect::frameCheck] Triggering aspect ration change! new ar: ${newAr}`, "color: #aaf");
|
|
||||||
|
|
||||||
// we also know edges for guardline, so set them.
|
|
||||||
// we need to be mindful of fallbackMode though
|
|
||||||
// if edges are okay and not invalid, we also
|
|
||||||
// allow automatic aspect ratio correction. If edges
|
|
||||||
// are bogus, we remain aspect ratio unchanged.
|
|
||||||
try {
|
|
||||||
if (!this.fallbackMode) {
|
|
||||||
// throws error if top/bottom are invalid
|
|
||||||
this.guardLine.setBlackbar({top: edgePost.guardLineTop, bottom: edgePost.guardLineBottom});
|
|
||||||
} else {
|
|
||||||
if (this.conf.player.dimensions){
|
|
||||||
this.guardLine.setBlackbarManual({
|
|
||||||
top: this.settings.active.arDetect.fallbackMode.noTriggerZonePx,
|
|
||||||
bottom: this.conf.player.dimensions.height - this.settings.active.arDetect.fallbackMode.noTriggerZonePx - 1
|
|
||||||
},{
|
|
||||||
top: edgePost.guardLineTop + this.settings.active.arDetect.guardLine.edgeTolerancePx,
|
|
||||||
bottom: edgePost.guardLineBottom - this.settings.active.arDetect.guardLine.edgeTolerancePx
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.processAr(newAr);
|
|
||||||
} catch (e) {
|
|
||||||
// edges weren't gucci, so we'll just reset
|
|
||||||
// the aspect ratio to defaults
|
|
||||||
this.logger.log('error', 'arDetect', `%c[ArDetect::frameCheck] There was a problem setting blackbar. Doing nothing. Error:`, e);
|
|
||||||
|
|
||||||
try {
|
|
||||||
this.guardLine.reset();
|
|
||||||
} catch (e) {
|
|
||||||
// no guardline, no bigge
|
|
||||||
}
|
|
||||||
// WE DO NOT RESET ASPECT RATIO HERE IN CASE OF PROBLEMS, CAUSES UNWARRANTED RESETS:
|
|
||||||
// (eg. here: https://www.youtube.com/watch?v=nw5Z93Yt-UQ&t=410)
|
|
||||||
//
|
|
||||||
// this.conf.resizer.setAr({type: AspectRatioType.Automatic, ratio: this.getDefaultAr()});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.clearImageData(imageData);
|
|
||||||
}
|
|
||||||
|
|
||||||
resetBlackLevel(){
|
|
||||||
this.blackLevel = this.settings.active.arDetect.blackbar.blackLevel;
|
|
||||||
}
|
|
||||||
|
|
||||||
blackLevelTest_full() {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
blackframeTest() {
|
|
||||||
if (this.blackLevel === undefined) {
|
|
||||||
this.logger.log('info', 'arDetect_verbose', "[ArDetect::blackframeTest] black level undefined, resetting");
|
|
||||||
this.resetBlackLevel();
|
|
||||||
}
|
|
||||||
|
|
||||||
const rows = this.blackframeCanvas.height;
|
|
||||||
const cols = this.blackframeCanvas.width;
|
|
||||||
const pixels = rows * cols;
|
|
||||||
let cumulative_r = 0, cumulative_g = 0, cumulative_b = 0;
|
|
||||||
let max_r = 0, max_g = 0, max_b = 0;
|
|
||||||
let avg_r, avg_g, avg_b;
|
|
||||||
let var_r = 0, var_g = 0, var_b = 0;
|
|
||||||
|
|
||||||
let pixelMax = 0;
|
|
||||||
let cumulativeValue = 0;
|
|
||||||
let blackPixelCount = 0;
|
|
||||||
const bfImageData = this.blackframeContext.getImageData(0, 0, cols, rows).data;
|
|
||||||
const blackTreshold = this.blackLevel + this.settings.active.arDetect.blackbar.frameThreshold;
|
|
||||||
|
|
||||||
|
|
||||||
// we do some recon for letterbox and pillarbox. While this can't determine whether letterbox/pillarbox exists
|
|
||||||
// with sufficient level of certainty due to small sample resolution, it can still give us some hints for later
|
|
||||||
let rowMax = new Array(rows).fill(0);
|
|
||||||
let colMax = new Array(cols).fill(0);
|
|
||||||
|
|
||||||
let r: number, c: number;
|
|
||||||
|
|
||||||
|
|
||||||
for (let i = 0; i < bfImageData.length; i+= 4) {
|
|
||||||
pixelMax = Math.max(bfImageData[i], bfImageData[i+1], bfImageData[i+2]);
|
|
||||||
bfImageData[i+3] = pixelMax;
|
|
||||||
|
|
||||||
if (pixelMax < blackTreshold) {
|
|
||||||
if (pixelMax < this.blackLevel) {
|
|
||||||
this.blackLevel = pixelMax;
|
|
||||||
}
|
|
||||||
blackPixelCount++;
|
|
||||||
} else {
|
|
||||||
cumulativeValue += pixelMax;
|
|
||||||
cumulative_r += bfImageData[i];
|
|
||||||
cumulative_g += bfImageData[i+1];
|
|
||||||
cumulative_b += bfImageData[i+2];
|
|
||||||
|
|
||||||
max_r = max_r > bfImageData[i] ? max_r : bfImageData[i];
|
|
||||||
max_g = max_g > bfImageData[i+1] ? max_g : bfImageData[i+1];
|
|
||||||
max_b = max_b > bfImageData[i+2] ? max_b : bfImageData[i+2];
|
|
||||||
}
|
|
||||||
|
|
||||||
r = ~~(i/rows);
|
|
||||||
c = i % cols;
|
|
||||||
|
|
||||||
if (pixelMax > rowMax[r]) {
|
|
||||||
rowMax[r] = pixelMax;
|
|
||||||
}
|
|
||||||
if (pixelMax > colMax[c]) {
|
|
||||||
colMax[c] = colMax;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
max_r = 1 / (max_r || 1);
|
|
||||||
max_g = 1 / (max_g || 1);
|
|
||||||
max_b = 1 / (max_b || 1);
|
|
||||||
|
|
||||||
const imagePixels = pixels - blackPixelCount;
|
|
||||||
// calculate averages and normalize them
|
|
||||||
avg_r = (cumulative_r / imagePixels) * max_r;
|
|
||||||
avg_g = (cumulative_g / imagePixels) * max_g;
|
|
||||||
avg_b = (cumulative_b / imagePixels) * max_b;
|
|
||||||
|
|
||||||
// second pass for color variance
|
|
||||||
for (let i = 0; i < bfImageData.length; i+= 4) {
|
|
||||||
if (bfImageData[i+3] >= this.blackLevel) {
|
|
||||||
var_r += Math.abs(avg_r - bfImageData[i] * max_r);
|
|
||||||
var_g += Math.abs(avg_g - bfImageData[i+1] * max_g);
|
|
||||||
var_b += Math.abs(avg_b - bfImageData[i+1] * max_b);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasSufficientVariance = Math.abs(var_r - var_g) / Math.max(var_r, var_g, 1) > this.settings.active.arDetect.blackframe.sufficientColorVariance
|
|
||||||
|| Math.abs(var_r - var_b) / Math.max(var_r, var_b, 1) > this.settings.active.arDetect.blackframe.sufficientColorVariance
|
|
||||||
|| Math.abs(var_b - var_g) / Math.max(var_b, var_g, 1) > this.settings.active.arDetect.blackframe.sufficientColorVariance
|
|
||||||
|
|
||||||
let isBlack = (blackPixelCount/(cols * rows) > this.settings.active.arDetect.blackframe.blackPixelsCondition);
|
|
||||||
|
|
||||||
if (! isBlack) {
|
|
||||||
if (hasSufficientVariance) {
|
|
||||||
isBlack = cumulativeValue < this.settings.active.arDetect.blackframe.cumulativeThresholdLax;
|
|
||||||
} else {
|
|
||||||
isBlack = cumulativeValue < this.settings.active.arDetect.blackframe.cumulativeThresholdStrict;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Debug.debug) {
|
|
||||||
return {
|
|
||||||
isBlack: isBlack,
|
|
||||||
blackPixelCount: blackPixelCount,
|
|
||||||
blackPixelRatio: (blackPixelCount/(cols * rows)),
|
|
||||||
cumulativeValue: cumulativeValue,
|
|
||||||
hasSufficientVariance: hasSufficientVariance,
|
|
||||||
blackLevel: this.blackLevel,
|
|
||||||
variances: {
|
|
||||||
raw: {
|
|
||||||
r: var_r, g: var_g, b: var_b
|
|
||||||
},
|
|
||||||
relative: {
|
|
||||||
rg: Math.abs(var_r - var_g) / Math.max(var_r, var_g, 1),
|
|
||||||
rb: Math.abs(var_r - var_b) / Math.max(var_r, var_b, 1),
|
|
||||||
gb: Math.abs(var_b - var_g) / Math.max(var_b, var_g, 1),
|
|
||||||
},
|
|
||||||
relativePercent: {
|
|
||||||
rg: Math.abs(var_r - var_g) / Math.max(var_r, var_g, 1) / this.settings.active.arDetect.blackframe.sufficientColorVariance,
|
|
||||||
rb: Math.abs(var_r - var_b) / Math.max(var_r, var_b, 1) / this.settings.active.arDetect.blackframe.sufficientColorVariance,
|
|
||||||
gb: Math.abs(var_b - var_g) / Math.max(var_b, var_g, 1) / this.settings.active.arDetect.blackframe.sufficientColorVariance,
|
|
||||||
},
|
|
||||||
varianceLimit: this.settings.active.arDetect.blackframe.sufficientColorVariance,
|
|
||||||
},
|
|
||||||
cumulativeValuePercent: cumulativeValue / (hasSufficientVariance ? this.settings.active.arDetect.blackframe.cumulativeThresholdLax : this.settings.active.arDetect.blackframe.cumulativeThresholdStrict),
|
|
||||||
rowMax: rowMax,
|
|
||||||
colMax: colMax,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
isBlack: isBlack,
|
|
||||||
rowMax: rowMax,
|
|
||||||
colMax: colMax,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fastLetterboxPresenceTest(imageData, sampleCols) {
|
|
||||||
// fast test to see if aspect ratio is correct.
|
|
||||||
// returns 'true' if presence of letterbox is possible.
|
|
||||||
// returns 'false' if we found a non-black edge pixel.
|
|
||||||
|
|
||||||
// If we detect anything darker than blackLevel, we modify blackLevel to the new lowest value
|
|
||||||
const rowOffset = this.canvas.width * (this.canvas.height - 1);
|
|
||||||
let currentMin = 255, currentMax = 0, colOffset_r, colOffset_g, colOffset_b, colOffset_rb, colOffset_gb, colOffset_bb, blthreshold = this.settings.active.arDetect.blackbar.threshold;
|
|
||||||
|
|
||||||
// detect black level. if currentMax comes above blackbar + blackbar threshold, we know we aren't letterboxed
|
|
||||||
|
|
||||||
for (let i = 0; i < sampleCols.length; ++i){
|
|
||||||
colOffset_r = sampleCols[i] << 2;
|
|
||||||
colOffset_g = colOffset_r + 1;
|
|
||||||
colOffset_b = colOffset_r + 2;
|
|
||||||
colOffset_rb = colOffset_r + rowOffset;
|
|
||||||
colOffset_gb = colOffset_g + rowOffset;
|
|
||||||
colOffset_bb = colOffset_b + rowOffset;
|
|
||||||
|
|
||||||
currentMax = Math.max(
|
|
||||||
imageData[colOffset_r], imageData[colOffset_g], imageData[colOffset_b],
|
|
||||||
// imageData[colOffset_rb], imageData[colOffset_gb], imageData[colOffset_bb],
|
|
||||||
currentMax
|
|
||||||
);
|
|
||||||
|
|
||||||
if (currentMax > this.blackLevel + blthreshold) {
|
|
||||||
// we search no further
|
|
||||||
if (currentMin < this.blackLevel) {
|
|
||||||
this.blackLevel = currentMin;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
currentMin = Math.min(
|
|
||||||
currentMax,
|
|
||||||
currentMin
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentMin < this.blackLevel)
|
|
||||||
this.blackLevel = currentMin
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
let _ard_console_stop = "background: #000; color: #f41";
|
|
||||||
let _ard_console_start = "background: #000; color: #00c399";
|
|
||||||
let _ard_console_change = "background: #000; color: #ff8";
|
|
||||||
|
|
||||||
export default ArDetector;
|
|
||||||
@ -1,129 +0,0 @@
|
|||||||
class DebugCanvas {
|
|
||||||
constructor(ardConf){
|
|
||||||
this.conf = ardConf;
|
|
||||||
this.targetWidth = 1280;
|
|
||||||
this.targetHeight = 720;
|
|
||||||
this.targetOffsetTop = 1080;
|
|
||||||
this.targetOffsetLeft = 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
init(canvasSize, canvasPosition, targetCanvasSize) {
|
|
||||||
console.log("initiating DebugCanvas")
|
|
||||||
|
|
||||||
var body = document.getElementsByTagName('body')[0];
|
|
||||||
|
|
||||||
if(!canvasPosition){
|
|
||||||
canvasPosition = {
|
|
||||||
top: 1200,
|
|
||||||
left: 800
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if(!this.canvas){
|
|
||||||
this.canvas = document.createElement("canvas");
|
|
||||||
body.appendChild(this.canvas);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(targetCanvasSize){
|
|
||||||
this.targetWidth = targetCanvasSize.width;
|
|
||||||
this.targetHeight = targetCanvasSize.height;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.canvas.style.position = "absolute";
|
|
||||||
this.canvas.style.left = `${canvasPosition.left}px`;
|
|
||||||
this.canvas.style.top = `${canvasPosition.top}px`;
|
|
||||||
this.canvas.style.zIndex = 10002;
|
|
||||||
// this.canvas.id = "uw_debug_canvas";
|
|
||||||
|
|
||||||
this.context = this.canvas.getContext("2d");
|
|
||||||
|
|
||||||
this.canvas.width = canvasSize.width;
|
|
||||||
this.canvas.height = canvasSize.height;
|
|
||||||
|
|
||||||
this.calculateCanvasZoom();
|
|
||||||
|
|
||||||
console.log("debug canvas is:", this.canvas, "context:", this.context)
|
|
||||||
}
|
|
||||||
|
|
||||||
calculateCanvasZoom(){
|
|
||||||
var canvasZoom = this.targetWidth / this.canvas.width;
|
|
||||||
var translateX = (this.canvas.width - this.targetWidth)/2;
|
|
||||||
var translateY = (this.canvas.height - this.targetHeight)/2;
|
|
||||||
|
|
||||||
this.canvas.style.transform = `scale(${canvasZoom},${canvasZoom}) translateX(${translateX}px) translateY(${translateY}px)`;
|
|
||||||
}
|
|
||||||
|
|
||||||
destroy(){
|
|
||||||
if(this.canvas)
|
|
||||||
this.canvas.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
setBuffer(buffer) {
|
|
||||||
// this.imageBuffer = buffer.splice(0);
|
|
||||||
this.imageBuffer = new Uint8ClampedArray(buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
trace(arrayIndex, colorClass) {
|
|
||||||
this.imageBuffer[arrayIndex ] = colorClass.colorRgb[0];
|
|
||||||
this.imageBuffer[arrayIndex+1] = colorClass.colorRgb[1];
|
|
||||||
this.imageBuffer[arrayIndex+2] = colorClass.colorRgb[2];
|
|
||||||
}
|
|
||||||
|
|
||||||
update() {
|
|
||||||
var start = performance.now();
|
|
||||||
try{
|
|
||||||
if(this.context && this.imageBuffer instanceof Uint8ClampedArray){
|
|
||||||
try{
|
|
||||||
var idata = new ImageData(this.imageBuffer, this.canvas.width, this.canvas.height);
|
|
||||||
} catch (ee) {
|
|
||||||
console.log("[DebugCanvas.js::update] can't create image data. Trying to correct canvas size. Error was:", ee);
|
|
||||||
this.canvas.width = this.conf.canvas.width;
|
|
||||||
this.canvas.height = this.conf.canvas.height;
|
|
||||||
|
|
||||||
this.calculateCanvasZoom();
|
|
||||||
// this.context = this.canvas.getContext("2d");
|
|
||||||
var idata = new ImageData(this.imageBuffer, this.canvas.width, this.canvas.height);
|
|
||||||
}
|
|
||||||
this.putImageData(this.context, idata, 0, 0, 0, 0, this.canvas.width, this.canvas.height);
|
|
||||||
}
|
|
||||||
} catch(e) {
|
|
||||||
console.log("[DebugCanvas.js::update] updating canvas failed.", e);
|
|
||||||
}
|
|
||||||
console.log("[DebugCanvas.js::update] update took", (performance.now() - start), "ms.");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
putImageData(ctx, imageData, dx, dy, dirtyX, dirtyY, dirtyWidth, dirtyHeight) {
|
|
||||||
var data = imageData.data;
|
|
||||||
var height = imageData.height;
|
|
||||||
var width = imageData.width;
|
|
||||||
dirtyX = dirtyX || 0;
|
|
||||||
dirtyY = dirtyY || 0;
|
|
||||||
dirtyWidth = dirtyWidth !== undefined? dirtyWidth: width;
|
|
||||||
dirtyHeight = dirtyHeight !== undefined? dirtyHeight: height;
|
|
||||||
var limitBottom = Math.min(dirtyHeight, height);
|
|
||||||
var limitRight = Math.min(dirtyWidth, width);
|
|
||||||
for (var y = dirtyY; y < limitBottom; y++) {
|
|
||||||
for (var x = dirtyX; x < limitRight; x++) {
|
|
||||||
var pos = y * width + x;
|
|
||||||
ctx.fillStyle = 'rgba(' + data[pos*4+0]
|
|
||||||
+ ',' + data[pos*4+1]
|
|
||||||
+ ',' + data[pos*4+2]
|
|
||||||
+ ',' + (data[pos*4+3]/255) + ')';
|
|
||||||
ctx.fillRect(x + dx, y + dy, 1, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DebugCanvasClasses = {
|
|
||||||
VIOLATION: {color: '#ff0000', colorRgb: [255, 0, 0], text: 'violation (general)'},
|
|
||||||
WARN: {color: '#d0d039', colorRgb: [208, 208, 57], text: 'lesser violation (general)'},
|
|
||||||
GUARDLINE_BLACKBAR: {color: '#3333FF', colorRgb: [51, 51, 255], text: 'guardline/blackbar (expected value)'},
|
|
||||||
GUARDLINE_IMAGE: {color: '#000088', colorRgb: [0, 0, 136], text: 'guardline/image (expected value)'},
|
|
||||||
|
|
||||||
EDGEDETECT_ONBLACK: {color: '#444444', colorRgb: [68, 68, 68], text: 'edge detect - perpendicular (to edge)'},
|
|
||||||
EDGEDETECT_CANDIDATE: {color: '#FFFFFF', colorRgb: [255, 255, 255], text: 'edge detect - edge candidate'},
|
|
||||||
EDGEDETECT_CANDIDATE_SECONDARY: {color: '#OOOOOO', colorRgb: [0, 0, 0], text: 'edge detect - edge candidate, but for when candidate is really bright'},
|
|
||||||
EDGEDETECT_BLACKBAR: {color: '#07ac93', colorRgb: [7, 172, 147], text: 'edge detect - parallel, black test'},
|
|
||||||
EDGEDETECT_IMAGE: {color: '#046c5c', colorRgb: [4, 108, 92], text: 'edge detect - parallel, image test'}
|
|
||||||
}
|
|
||||||
@ -1,326 +0,0 @@
|
|||||||
import Debug from '../../conf/Debug';
|
|
||||||
import Settings from '../Settings';
|
|
||||||
import ArDetector from './ArDetector';
|
|
||||||
|
|
||||||
export type GuardLineBar = {
|
|
||||||
top?: number;
|
|
||||||
bottom?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ImageCheckResult = {
|
|
||||||
success: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
class GuardLine {
|
|
||||||
blackbar: GuardLineBar;
|
|
||||||
imageBar: GuardLineBar;
|
|
||||||
|
|
||||||
aard: ArDetector;
|
|
||||||
settings: Settings;
|
|
||||||
|
|
||||||
blackbarThreshold: number;
|
|
||||||
imageThreshold: number;
|
|
||||||
|
|
||||||
|
|
||||||
// ardConf — reference to ArDetector that has current GuardLine instance
|
|
||||||
constructor (ardConf){
|
|
||||||
this.blackbar = {top: undefined, bottom: undefined};
|
|
||||||
this.imageBar = {top: undefined, bottom: undefined};
|
|
||||||
|
|
||||||
this.aard = ardConf;
|
|
||||||
this.settings = ardConf.settings;
|
|
||||||
}
|
|
||||||
|
|
||||||
reset() {
|
|
||||||
this.blackbar = {top: undefined, bottom: undefined};
|
|
||||||
this.imageBar = {top: undefined, bottom: undefined};
|
|
||||||
}
|
|
||||||
|
|
||||||
setBlackbarManual(blackbarConf, imagebarConf){
|
|
||||||
// ni lepo uporabljat tega, ampak pri fallback mode nastavljamo blackbar stuff na roke
|
|
||||||
// it's not nice to use this, but we're setting these values manually in fallbackMode
|
|
||||||
if (blackbarConf) {
|
|
||||||
this.blackbar = blackbarConf;
|
|
||||||
}
|
|
||||||
if (imagebarConf) {
|
|
||||||
this.imageBar = imagebarConf;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setBlackbar(bbconf){
|
|
||||||
let bbTop = bbconf.top - this.settings.active.arDetect.guardLine.edgeTolerancePx;
|
|
||||||
let bbBottom = bbconf.bottom + this.settings.active.arDetect.guardLine.edgeTolerancePx;
|
|
||||||
|
|
||||||
// to odstrani vse neveljavne nastavitve in vse možnosti, ki niso smiselne
|
|
||||||
// this removes any configs with invalid values or values that dont make sense
|
|
||||||
if (bbTop < 0 || bbBottom >= this.aard.canvas.height ){
|
|
||||||
throw {error: "INVALID_SETTINGS_IN_GUARDLINE", bbTop, bbBottom}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.blackbar = {
|
|
||||||
top: bbTop,
|
|
||||||
bottom: bbBottom
|
|
||||||
}
|
|
||||||
|
|
||||||
this.imageBar = {
|
|
||||||
top: bbconf.top + 1 + this.settings.active.arDetect.guardLine.edgeTolerancePx,
|
|
||||||
bottom: bbconf.bottom - 1 - this.settings.active.arDetect.guardLine.edgeTolerancePx
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
check(image, fallbackMode){
|
|
||||||
// izračunaj enkrat in shrani na objekt
|
|
||||||
// calculate once and save object-instance-wide
|
|
||||||
this.blackbarThreshold = this.aard.blackLevel + this.settings.active.arDetect.blackbar.threshold;
|
|
||||||
this.imageThreshold = this.blackbarThreshold + this.settings.active.arDetect.blackbar.imageThreshold;
|
|
||||||
|
|
||||||
// dejansko testiranje
|
|
||||||
// actual checks
|
|
||||||
let guardLineResult = this.guardLineCheck(image, fallbackMode);
|
|
||||||
|
|
||||||
// Zaznali smo kršitev črnega dela, zato nam ni treba preveriti, ali je slika
|
|
||||||
// prisotna. Vemo namreč, da se je razmerje stranic zmanjšalo.
|
|
||||||
//
|
|
||||||
// blackbar violation detected. We don't even need to check for presence of image
|
|
||||||
// as aspect ratio just decreased
|
|
||||||
if(! guardLineResult.success) {
|
|
||||||
return {
|
|
||||||
blackbarFail: true,
|
|
||||||
offenders: guardLineResult.offenders,
|
|
||||||
imageFail: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let imageCheckResult = this.imageCheck(image, fallbackMode);
|
|
||||||
|
|
||||||
return {
|
|
||||||
blackbarFail: false,
|
|
||||||
imageFail: ! imageCheckResult.success
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// don't use methods below this line outside this class
|
|
||||||
guardLineCheck(image, fallbackMode){
|
|
||||||
// this test tests for whether we crop too aggressively
|
|
||||||
|
|
||||||
// if this test is passed, then aspect ratio probably didn't change from wider to narrower. However, further
|
|
||||||
// checks are needed to determine whether aspect ratio got wider.
|
|
||||||
// if this test fails, it returns a list of offending points.
|
|
||||||
|
|
||||||
// if the upper edge is null, then edge hasn't been detected before. This test is pointless, therefore it
|
|
||||||
// should succeed by default. Also need to check bottom, for cases where only one edge is known
|
|
||||||
|
|
||||||
if(! fallbackMode && (! this.blackbar.top || ! this.blackbar.bottom)) {
|
|
||||||
return { success: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
let offset = (this.aard.canvas.width * this.settings.active.arDetect.guardLine.ignoreEdgeMargin) << 2;
|
|
||||||
|
|
||||||
let offenders = [];
|
|
||||||
let offenderCount = -1; // doing it this way means first offender has offenderCount==0. Ez index.
|
|
||||||
|
|
||||||
// TODO: implement logo check.
|
|
||||||
|
|
||||||
// preglejmo obe vrstici
|
|
||||||
// check both rows
|
|
||||||
let edge_lower, edge_upper;
|
|
||||||
|
|
||||||
if(! fallbackMode){
|
|
||||||
edge_upper = this.blackbar.top;
|
|
||||||
edge_lower = this.blackbar.bottom;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// fallback mode is a bit different
|
|
||||||
edge_upper = 0;
|
|
||||||
edge_lower = this.aard.canvas.height - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
let rowStart, rowEnd;
|
|
||||||
|
|
||||||
// <<<=======| checking upper row |========>>>
|
|
||||||
|
|
||||||
rowStart = ((edge_upper * this.aard.canvas.width) << 2) + offset;
|
|
||||||
rowEnd = rowStart + ( this.aard.canvas.width << 2 ) - (offset * 2);
|
|
||||||
|
|
||||||
if (Debug.debugCanvas.enabled && Debug.debugCanvas.guardLine) {
|
|
||||||
// offenderCount = this._gl_debugRowCheck(image, rowStart, rowEnd, offenders, offenderCount);
|
|
||||||
} else {
|
|
||||||
offenderCount = this._gl_rowCheck(image, rowStart, rowEnd, offenders, offenderCount);
|
|
||||||
}
|
|
||||||
// <<<=======| checking lower row |========>>>
|
|
||||||
|
|
||||||
rowStart = ((edge_lower * this.aard.canvas.width) << 2) + offset;
|
|
||||||
rowEnd = rowStart + ( this.aard.canvas.width << 2 ) - (offset * 2);
|
|
||||||
|
|
||||||
if (Debug.debugCanvas.enabled && Debug.debugCanvas.guardLine) {
|
|
||||||
// offenderCount = this._gl_debugRowCheck(image, rowStart, rowEnd, offenders, offenderCount);
|
|
||||||
} else {
|
|
||||||
offenderCount = this._gl_rowCheck(image, rowStart, rowEnd, offenders, offenderCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
// če nismo našli nobenih prekrškarjev, vrnemo uspeh. Drugače vrnemo seznam prekrškarjev
|
|
||||||
// vrnemo tabelo, ki vsebuje sredinsko točko vsakega prekrškarja (x + width*0.5)
|
|
||||||
//
|
|
||||||
// if we haven't found any offenders, we return success. Else we return list of offenders
|
|
||||||
// we return array of middle points of offenders (x + (width * 0.5) for every offender)
|
|
||||||
|
|
||||||
if(offenderCount == -1){
|
|
||||||
return {success: true};
|
|
||||||
}
|
|
||||||
|
|
||||||
let ret = new Array(offenders.length);
|
|
||||||
for(let o in offenders){
|
|
||||||
ret[o] = offenders[o].x + (offenders[o].width * 0.25);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {success: false, offenders: ret};
|
|
||||||
}
|
|
||||||
|
|
||||||
imageCheck(image, fallbackMode?: boolean): ImageCheckResult {
|
|
||||||
if(!this.imageBar.top || !this.imageBar.bottom)
|
|
||||||
return { success: false };
|
|
||||||
|
|
||||||
let offset = (this.aard.canvas.width * this.settings.active.arDetect.guardLine.ignoreEdgeMargin) << 2;
|
|
||||||
|
|
||||||
// TODO: implement logo check.
|
|
||||||
|
|
||||||
|
|
||||||
// preglejmo obe vrstici - tukaj po pravilih ne bi smeli iti prek mej platna. ne rabimo preverjati
|
|
||||||
// check both rows - by the rules and definitions, we shouldn't go out of bounds here. no need to check, then
|
|
||||||
|
|
||||||
// if(fallbackMode){
|
|
||||||
// let edge_upper = this.settings.active.arDetect.fallbackMode.noTriggerZonePx;
|
|
||||||
// let edge_lower = this.conf.canvas.height - this.settings.active.arDetect.fallbackMode.noTriggerZonePx - 1;
|
|
||||||
// }
|
|
||||||
// else{
|
|
||||||
let edge_upper = this.imageBar.top;
|
|
||||||
let edge_lower = this.imageBar.bottom;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// koliko pikslov rabimo zaznati, da je ta funkcija uspe. Tu dovoljujemo tudi, da so vsi piksli na enem
|
|
||||||
// robu (eden izmed robov je lahko v celoti črn)
|
|
||||||
// how many non-black pixels we need to consider this check a success. We only need to detect enough pixels
|
|
||||||
// on one edge (one of the edges can be black as long as both aren't)
|
|
||||||
let successThreshold = (this.aard.canvas.width * this.settings.active.arDetect.guardLine.imageTestThreshold);
|
|
||||||
let rowStart, rowEnd;
|
|
||||||
|
|
||||||
|
|
||||||
// <<<=======| checking upper row |========>>>
|
|
||||||
|
|
||||||
rowStart = ((edge_upper * this.aard.canvas.width) << 2) + offset;
|
|
||||||
rowEnd = rowStart + ( this.aard.canvas.width << 2 ) - (offset * 2);
|
|
||||||
|
|
||||||
let res = false;
|
|
||||||
|
|
||||||
if(Debug.debugCanvas.enabled && Debug.debugCanvas.guardLine){
|
|
||||||
// res = this._ti_debugCheckRow(image, rowStart, rowEnd, successThreshold);
|
|
||||||
} else {
|
|
||||||
res = this._ti_checkRow(image, rowStart, rowEnd,successThreshold);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (res) {
|
|
||||||
return {success: true};
|
|
||||||
}
|
|
||||||
|
|
||||||
// <<<=======| checking lower row |========>>>
|
|
||||||
|
|
||||||
rowStart = ((edge_lower * this.aard.canvas.width) << 2) + offset;
|
|
||||||
// rowEnd = rowStart + ( this.conf.canvas.width << 2 ) - (offset * 2);
|
|
||||||
|
|
||||||
|
|
||||||
if(Debug.debugCanvas.enabled && Debug.debugCanvas.guardLine){
|
|
||||||
// res = this._ti_debugCheckRow(image, rowStart, rowEnd, successThreshold);
|
|
||||||
} else {
|
|
||||||
res = this._ti_checkRow(image, rowStart, rowEnd,successThreshold);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {success: res};
|
|
||||||
}
|
|
||||||
|
|
||||||
// pomožne metode
|
|
||||||
// helper methods
|
|
||||||
|
|
||||||
|
|
||||||
_gl_rowCheck(image, rowStart, rowEnd, offenders, offenderCount){
|
|
||||||
let firstOffender = -1;
|
|
||||||
for(let i = rowStart; i < rowEnd; i+=4){
|
|
||||||
|
|
||||||
// we track sections that go over what's supposed to be a black line, so we can suggest more
|
|
||||||
// columns to sample
|
|
||||||
if(image[i] > this.blackbarThreshold || image[i+1] > this.blackbarThreshold || image[i+2] > this.blackbarThreshold){
|
|
||||||
if(firstOffender < 0){
|
|
||||||
firstOffender = (i - rowStart) >> 2;
|
|
||||||
offenderCount++;
|
|
||||||
offenders.push({x: firstOffender, width: 1});
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
offenders[offenderCount].width++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
// is that a black pixel again? Let's reset the 'first offender'
|
|
||||||
firstOffender = -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return offenderCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
// _gl_debugRowCheck(image, rowStart, rowEnd, offenders, offenderCount){
|
|
||||||
// let firstOffender = -1;
|
|
||||||
// for(let i = rowStart; i < rowEnd; i+=4){
|
|
||||||
|
|
||||||
// // we track sections that go over what's supposed to be a black line, so we can suggest more
|
|
||||||
// // columns to sample
|
|
||||||
// if(image[i] > this.blackbarThreshold || image[i+1] > this.blackbarThreshold || image[i+2] > this.blackbarThreshold){
|
|
||||||
// this.aard.debugCanvas.trace(i, DebugCanvasClasses.VIOLATION);
|
|
||||||
// if(firstOffender < 0){
|
|
||||||
// firstOffender = (i - rowStart) >> 2;
|
|
||||||
// offenderCount++;
|
|
||||||
// offenders.push({x: firstOffender, width: 1});
|
|
||||||
// }
|
|
||||||
// else{
|
|
||||||
// offenders[offenderCount].width++
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// else{
|
|
||||||
// this.aard.debugCanvas.trace(i, DebugCanvasClasses.GUARDLINE_BLACKBAR);
|
|
||||||
// // is that a black pixel again? Let's reset the 'first offender'
|
|
||||||
// firstOffender = -1;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return offenderCount;
|
|
||||||
// }
|
|
||||||
|
|
||||||
_ti_checkRow(image, rowStart, rowEnd, successThreshold): boolean {
|
|
||||||
for(let i = rowStart; i < rowEnd; i+=4){
|
|
||||||
if(image[i] > this.imageThreshold || image[i+1] > this.imageThreshold || image[i+2] > this.imageThreshold){
|
|
||||||
if(successThreshold --<= 0){
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// _ti_debugCheckRow(image, rowStart, rowEnd, successThreshold) {
|
|
||||||
// for(let i = rowStart; i < rowEnd; i+=4){
|
|
||||||
// if(image[i] > this.imageThreshold || image[i+1] > this.imageThreshold || image[i+2] > this.imageThreshold){
|
|
||||||
// this.aard.debugCanvas.trace(i, DebugCanvasClasses.GUARDLINE_IMAGE);
|
|
||||||
// if(successThreshold --<= 0){
|
|
||||||
// return true;
|
|
||||||
// }
|
|
||||||
// } else {
|
|
||||||
// this.aard.debugCanvas.trace(i, DebugCanvasClasses.WARN);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return false;
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
export default GuardLine;
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,6 +0,0 @@
|
|||||||
enum EdgeDetectPrimaryDirection {
|
|
||||||
Vertical = 0,
|
|
||||||
Horizontal = 1
|
|
||||||
};
|
|
||||||
|
|
||||||
export default EdgeDetectPrimaryDirection;
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
enum EdgeDetectQuality {
|
|
||||||
Fast = 0,
|
|
||||||
Improved = 1
|
|
||||||
};
|
|
||||||
|
|
||||||
export default EdgeDetectQuality;
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
enum EdgeStatus {
|
|
||||||
ARUnknown = 0,
|
|
||||||
ARKnown = 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default EdgeStatus;
|
|
||||||
@ -1,130 +0,0 @@
|
|||||||
import Debug from '../../conf/Debug';
|
|
||||||
import BrowserDetect from '../../conf/BrowserDetect';
|
|
||||||
import Logger from '../Logger';
|
|
||||||
import { browser } from 'webextension-polyfill-ts';
|
|
||||||
import Settings from '../Settings';
|
|
||||||
|
|
||||||
if (process.env.CHANNEL !== 'stable'){
|
|
||||||
console.info("Loading CommsClient");
|
|
||||||
}
|
|
||||||
|
|
||||||
class CommsClient {
|
|
||||||
commsId: string;
|
|
||||||
|
|
||||||
logger: Logger;
|
|
||||||
settings: any; // sus?
|
|
||||||
|
|
||||||
|
|
||||||
commands: {[x: string]: any[]};
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
_listener: (m: any) => void;
|
|
||||||
port: any;
|
|
||||||
|
|
||||||
|
|
||||||
constructor(name, logger, commands) {
|
|
||||||
try {
|
|
||||||
this.logger = logger;
|
|
||||||
|
|
||||||
this.port = browser.runtime.connect(null, {name: name});
|
|
||||||
|
|
||||||
this.logger.onLogEnd(
|
|
||||||
(history) => {
|
|
||||||
this.logger.log('info', 'comms', 'Sending logging-stop-and-save to background script ...');
|
|
||||||
try {
|
|
||||||
this.port.postMessage({cmd: 'logging-stop-and-save', host: window.location.hostname, history})
|
|
||||||
} catch (e) {
|
|
||||||
this.logger.log('error', 'comms', 'Failed to send message to background script. Error:', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
this._listener = m => this.processReceivedMessage(m);
|
|
||||||
this.port.onMessage.addListener(this._listener);
|
|
||||||
|
|
||||||
this.commsId = (Math.random() * 20).toFixed(0);
|
|
||||||
|
|
||||||
this.commands = commands;
|
|
||||||
} catch (e) {
|
|
||||||
console.error("CONSTRUCOTR FAILED:", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
destroy() {
|
|
||||||
if (!BrowserDetect.edge) { // edge is a very special browser made by outright morons.
|
|
||||||
this.port.onMessage.removeListener(this._listener);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
subscribe(command, callback) {
|
|
||||||
if (!this.commands[command]) {
|
|
||||||
this.commands[command] = [callback];
|
|
||||||
} else {
|
|
||||||
this.commands[command].push(callback);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
processReceivedMessage(message){
|
|
||||||
this.logger.log('info', 'comms', `[CommsClient.js::processMessage] <${this.commsId}> Received message from background script!`, message);
|
|
||||||
|
|
||||||
if (this.commands[message.cmd]) {
|
|
||||||
for (const c of this.commands[message.cmd]) {
|
|
||||||
c(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async sendMessage_nonpersistent(message){
|
|
||||||
message = JSON.parse(JSON.stringify(message)); // vue quirk. We should really use vue store instead
|
|
||||||
return browser.runtime.sendMessage(null, message, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// TODO: sus function — does it get any use?
|
|
||||||
async requestSettings(){
|
|
||||||
this.logger.log('info', 'comms', "%c[CommsClient::requestSettings] sending request for congif!", "background: #11D; color: #aad");
|
|
||||||
|
|
||||||
var response = await this.sendMessage_nonpersistent({cmd: 'get-config'});
|
|
||||||
|
|
||||||
this.logger.log('info', 'comms', "%c[CommsClient::requestSettings] received settings response!", "background: #11D; color: #aad", response);
|
|
||||||
|
|
||||||
if(! response || response.extensionConf){
|
|
||||||
return Promise.resolve(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.settings = {active: JSON.parse(response.extensionConf)};
|
|
||||||
return Promise.resolve(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
async sendMessage(message) {
|
|
||||||
return this.sendMessage_nonpersistent(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
registerVideo(){
|
|
||||||
this.logger.log('info', 'comms', `[CommsClient::registerVideo] <${this.commsId}>`, "Registering video for current page.");
|
|
||||||
this.port.postMessage({cmd: "has-video"});
|
|
||||||
}
|
|
||||||
|
|
||||||
sendPerformanceUpdate(message){
|
|
||||||
this.port.postMessage({cmd: 'performance-update', message: message});
|
|
||||||
}
|
|
||||||
|
|
||||||
unregisterVideo(){
|
|
||||||
this.logger.log('info', 'comms', `[CommsClient::unregisterVideo] <${this.commsId}>`, "Unregistering video for current page.");
|
|
||||||
this.port.postMessage({cmd: "noVideo"}); // ayymd
|
|
||||||
}
|
|
||||||
|
|
||||||
announceZoom(scale){
|
|
||||||
this.port.postMessage({cmd: "announce-zoom", zoom: scale});
|
|
||||||
this.registerVideo();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.env.CHANNEL !== 'stable'){
|
|
||||||
console.info("CommsClient loaded");
|
|
||||||
}
|
|
||||||
|
|
||||||
export default CommsClient;
|
|
||||||
@ -1,347 +0,0 @@
|
|||||||
import Debug from '../../conf/Debug';
|
|
||||||
import BrowserDetect from '../../conf/BrowserDetect';
|
|
||||||
import Logger from '../Logger';
|
|
||||||
import Settings from '../Settings';
|
|
||||||
import { browser } from 'webextension-polyfill-ts';
|
|
||||||
import ExtensionMode from '../../../common/enums/ExtensionMode.enum';
|
|
||||||
|
|
||||||
|
|
||||||
class CommsServer {
|
|
||||||
server: any;
|
|
||||||
logger: Logger;
|
|
||||||
settings: Settings;
|
|
||||||
|
|
||||||
|
|
||||||
ports: {
|
|
||||||
[frame: string] : {
|
|
||||||
[port: string]: any
|
|
||||||
}
|
|
||||||
}[] = [];
|
|
||||||
popupPort: any;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* commands — functions that handle incoming messages
|
|
||||||
* functions can have the following arguments, which are,
|
|
||||||
* in this order:
|
|
||||||
* message — the message we received
|
|
||||||
* port|sender — on persistent channels, second argument is port on which the server
|
|
||||||
* listens. If the message was sent in non-persistent way, this is the
|
|
||||||
* sender script/frame/whatever of the message
|
|
||||||
* sendResponse — callback function on messages received via non-persistent channel
|
|
||||||
*/
|
|
||||||
commands: {[x: string]: ((a: any, b: any) => void | Promise<void>)[]} = {
|
|
||||||
'announce-zoom': [
|
|
||||||
(message) => {
|
|
||||||
try {
|
|
||||||
// forward message to the popup
|
|
||||||
this.popupPort.postMessage({cmd: 'set-current-zoom', zoom: message.zoom});
|
|
||||||
} catch (e) {
|
|
||||||
// if popup is closed, this will/may fail. This is okay, so we just ignore this error
|
|
||||||
}
|
|
||||||
},
|
|
||||||
],
|
|
||||||
'get-current-zoom': [
|
|
||||||
(message) => this.sendToActive(message),
|
|
||||||
],
|
|
||||||
'get-current-site': [
|
|
||||||
async (message, port) => {
|
|
||||||
port.postMessage({
|
|
||||||
cmd: 'set-current-site',
|
|
||||||
site: await this.server.getVideoTab(),
|
|
||||||
tabHostname: await this.getCurrentTabHostname()
|
|
||||||
});
|
|
||||||
},
|
|
||||||
],
|
|
||||||
'popup-set-selected-tab': [
|
|
||||||
(message) => this.server.setSelectedTab(message.selectedMenu, message.selectedSubitem),
|
|
||||||
],
|
|
||||||
'has-video': [
|
|
||||||
(message, port) => this.server.registerVideo(port.sender),
|
|
||||||
],
|
|
||||||
'noVideo': [
|
|
||||||
(message, port) => this.server.unregisterVideo(port.sender),
|
|
||||||
],
|
|
||||||
'inject-css': [
|
|
||||||
(message, sender) => this.server.injectCss(message.cssString, sender),
|
|
||||||
],
|
|
||||||
'eject-css': [
|
|
||||||
(message, sender) => this.server.removeCss(message.cssString, sender),
|
|
||||||
],
|
|
||||||
'replace-css': [
|
|
||||||
(message, sender) => this.server.replaceCss(message.oldCssString, message.newCssString, sender),
|
|
||||||
],
|
|
||||||
// 'get-config': [
|
|
||||||
// (message, port) => {
|
|
||||||
// this.logger.log('info', 'comms', "CommsServer: received get-config. Active settings?", this.settings.active, "\n(settings:", this.settings, ")");
|
|
||||||
// port.postMessage(
|
|
||||||
// {cmd: "set-config", conf: this.settings.active, site: this.server.currentSite}
|
|
||||||
// );
|
|
||||||
// },
|
|
||||||
// ],
|
|
||||||
'get-config': [
|
|
||||||
(message, sender) => {
|
|
||||||
var ret = {extensionConf: JSON.stringify(this.settings.active)};
|
|
||||||
this.logger.log('info', 'comms', "%c[CommsServer.js::processMessage_nonpersistent] Returning this:", "background-color: #11D; color: #aad", ret);
|
|
||||||
Promise.resolve(ret);
|
|
||||||
}
|
|
||||||
],
|
|
||||||
'autoar-enable': [
|
|
||||||
() => {
|
|
||||||
this.settings.active.sites['@global'].autoar = ExtensionMode.Enabled;
|
|
||||||
this.settings.save();
|
|
||||||
this.logger.log('info', 'comms', "[uw-bg] autoar set to enabled (blacklist). evidenz:", this.settings.active);
|
|
||||||
}
|
|
||||||
],
|
|
||||||
'autoar-disable': [
|
|
||||||
(message) => {
|
|
||||||
this.settings.active.sites['@global'].autoar = ExtensionMode.Disabled;
|
|
||||||
if (message.reason){
|
|
||||||
this.settings.active.arDetect.disabledReason = message.reason;
|
|
||||||
} else {
|
|
||||||
this.settings.active.arDetect.disabledReason = 'User disabled';
|
|
||||||
}
|
|
||||||
this.settings.save();
|
|
||||||
this.logger.log('info', 'comms', "[uw-bg] autoar set to disabled. evidenz:", this.settings.active);
|
|
||||||
}
|
|
||||||
],
|
|
||||||
'autoar-set-interval': [
|
|
||||||
(message) => {
|
|
||||||
this.logger.log('info', 'comms', `[uw-bg] trying to set new interval for autoAr. New interval is, ${message.timeout} ms`);
|
|
||||||
|
|
||||||
// set fairly liberal limit
|
|
||||||
var timeout = message.timeout < 4 ? 4 : message.timeout;
|
|
||||||
this.settings.active.arDetect.timers.playing = timeout;
|
|
||||||
this.settings.save();
|
|
||||||
}
|
|
||||||
],
|
|
||||||
'logging-stop-and-save': [ // TODO: possibly never used/superseded — check
|
|
||||||
(message, sender) => {
|
|
||||||
this.logger.log('info', 'comms', "Received command to stop logging and export the received input");
|
|
||||||
this.logger.addToGlobalHistory(`${message.host}::${sender?.tab?.id ?? '×'}-${sender.frameId ?? '×'}`, JSON.parse(message.history));
|
|
||||||
this.logger.finish();
|
|
||||||
}
|
|
||||||
],
|
|
||||||
'logging-save': [
|
|
||||||
(message, sender) => {
|
|
||||||
this.logger.log('info', 'comms', `Received command to save log for site ${message.host} (tabId ${sender.tab.id}, frameId ${sender.frameId}`);
|
|
||||||
this.logger.addToGlobalHistory(`${message?.host}::${sender?.tab?.id ?? '×'}-${sender?.frameId ?? '×'}`, JSON.parse(message.history));
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//#region getters
|
|
||||||
get activeTab() {
|
|
||||||
return browser.tabs.query({currentWindow: true, active: true});
|
|
||||||
}
|
|
||||||
//#endregion
|
|
||||||
|
|
||||||
constructor(server) {
|
|
||||||
this.server = server;
|
|
||||||
this.logger = server.logger;
|
|
||||||
this.settings = server.settings;
|
|
||||||
|
|
||||||
browser.runtime.onConnect.addListener(p => this.onConnect(p));
|
|
||||||
browser.runtime.onMessage.addListener((m, sender) => this.processReceivedMessage_nonpersistent(m, sender));
|
|
||||||
}
|
|
||||||
|
|
||||||
subscribe(command, callback) {
|
|
||||||
if (!this.commands[command]) {
|
|
||||||
this.commands[command] = [callback];
|
|
||||||
} else {
|
|
||||||
this.commands[command].push(callback);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getCurrentTabHostname() {
|
|
||||||
const activeTab = await this.activeTab;
|
|
||||||
|
|
||||||
if (!activeTab || activeTab.length < 1) {
|
|
||||||
this.logger.log('warn', 'comms', 'There is no active tab for some reason. activeTab:', activeTab);
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = activeTab[0].url;
|
|
||||||
|
|
||||||
var hostname;
|
|
||||||
|
|
||||||
if (url.indexOf("://") > -1) { //find & remove protocol (http, ftp, etc.) and get hostname
|
|
||||||
hostname = url.split('/')[2];
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
hostname = url.split('/')[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
hostname = hostname.split(':')[0]; //find & remove port number
|
|
||||||
hostname = hostname.split('?')[0]; //find & remove "?"
|
|
||||||
|
|
||||||
return hostname;
|
|
||||||
}
|
|
||||||
|
|
||||||
sendToAll(message){
|
|
||||||
for(const tab of this.ports){
|
|
||||||
for(const frame in tab){
|
|
||||||
for (const port in tab[frame]) {
|
|
||||||
tab[frame][port].postMessage(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends a message to addon content scripts.
|
|
||||||
* @param message message
|
|
||||||
* @param tab the tab we want to send the message to
|
|
||||||
* @param frame the frame within that tab that we want to send the message to
|
|
||||||
* @param port if defined, message will only be sent to that specific script, otherwise it gets sent to all scripts of a given frame
|
|
||||||
*/
|
|
||||||
async sendToFrameContentScripts(message, tab, frame, port?) {
|
|
||||||
if (port !== undefined) {
|
|
||||||
this.ports[tab][frame][port].postMessage(message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (const framePort in this.ports[tab][frame]) {
|
|
||||||
this.ports[tab][frame][framePort].postMessage(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async sendToFrame(message, tab, frame, port?) {
|
|
||||||
this.logger.log('info', 'comms', `%c[CommsServer::sendToFrame] attempting to send message to tab ${tab}, frame ${frame}`, "background: #dda; color: #11D", message);
|
|
||||||
|
|
||||||
if (isNaN(tab)) {
|
|
||||||
if (frame === '__playing') {
|
|
||||||
message['playing'] = true;
|
|
||||||
this.sendToAll(message);
|
|
||||||
return;
|
|
||||||
} else if (frame === '__all') {
|
|
||||||
this.sendToAll(message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
[tab, frame] = frame.split('-');
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.log('info', 'comms', `%c[CommsServer::sendToFrame] attempting to send message to tab ${tab}, frame ${frame}`, "background: #dda; color: #11D", message);
|
|
||||||
|
|
||||||
try {
|
|
||||||
this.sendToFrameContentScripts(message, tab, frame, port);
|
|
||||||
} catch (e) {
|
|
||||||
this.logger.log('error', 'comms', `%c[CommsServer::sendToFrame] Sending message failed. Reason:`, "background: #dda; color: #11D", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async sendToAllFrames(message, tab, port) {
|
|
||||||
for (const frame in this.ports[tab]) {
|
|
||||||
this.sendToFrameContentScripts(message, tab, frame, port);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async sendToActive(message) {
|
|
||||||
this.logger.log('info', 'comms', "%c[CommsServer::sendToActive] trying to send a message to active tab. Message:", "background: #dda; color: #11D", message);
|
|
||||||
|
|
||||||
const tabs = await this.activeTab;
|
|
||||||
|
|
||||||
this.logger.log('info', 'comms', "[CommsServer::_sendToActive] currently active tab(s)?", tabs);
|
|
||||||
for (const frame in this.ports[tabs[0].id]) {
|
|
||||||
this.logger.log('info', 'comms', "key?", frame, this.ports[tabs[0].id]);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const frame in this.ports[tabs[0].id]) {
|
|
||||||
this.sendToFrameContentScripts(message, tabs[0].id, frame);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onConnect(port){
|
|
||||||
// poseben primer | special case
|
|
||||||
if (port.name === 'popup-port') {
|
|
||||||
this.popupPort = port;
|
|
||||||
this.popupPort.onMessage.addListener( (m,p) => this.processReceivedMessage(m,p));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var tabId = port.sender.tab.id;
|
|
||||||
var frameId = port.sender.frameId;
|
|
||||||
if (! this.ports[tabId]){
|
|
||||||
this.ports[tabId] = {};
|
|
||||||
}
|
|
||||||
if (! this.ports[tabId][frameId]) {
|
|
||||||
this.ports[tabId][frameId] = {};
|
|
||||||
}
|
|
||||||
this.ports[tabId][frameId][port.name] = port;
|
|
||||||
this.ports[tabId][frameId][port.name].onMessage.addListener( (m,p) => this.processReceivedMessage(m, p));
|
|
||||||
|
|
||||||
this.ports[tabId][frameId][port.name].onDisconnect.addListener( (p) => {
|
|
||||||
try {
|
|
||||||
delete this.ports[p.sender.tab.id][p.sender.frameId][port.name];
|
|
||||||
} catch (e) {
|
|
||||||
// no biggie if the thing above doesn't exist.
|
|
||||||
}
|
|
||||||
if (Object.keys(this.ports[tabId][frameId].length === 0)) {
|
|
||||||
delete this.ports[tabId][frameId];
|
|
||||||
if(Object.keys(this.ports[p.sender.tab.id]).length === 0) {
|
|
||||||
delete this.ports[tabId];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// TODO: sendResponse seems redundant — it used to be a callback for
|
|
||||||
// chrome-based browsers, but browser polyfill doesn't do callback. Just
|
|
||||||
// awaits.
|
|
||||||
async execCmd(message, portOrSender, sendResponse?) {
|
|
||||||
this.logger.log(
|
|
||||||
'info', 'comms', '[CommsServer.js::execCmd] Received message', message,
|
|
||||||
". Port/sender:", portOrSender, "sendResponse:", sendResponse, "\nThere is ", this.commands[message.cmd]?.length ?? 0,
|
|
||||||
" command(s) for action", message.cmd
|
|
||||||
);
|
|
||||||
if (this.commands[message.cmd]) {
|
|
||||||
for (const c of this.commands[message.cmd]) {
|
|
||||||
try {
|
|
||||||
await c(message, portOrSender);
|
|
||||||
} catch (e) {
|
|
||||||
this.logger.log('error', 'debug', "[CommsServer.js::execCmd] failed to execute command.", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async handleMessage(message, portOrSender) {
|
|
||||||
await this.execCmd(message, portOrSender);
|
|
||||||
|
|
||||||
if (message.forwardToSameFramePort) {
|
|
||||||
this.sendToFrameContentScripts(message, portOrSender.tab.id, portOrSender.frameId, message.port);
|
|
||||||
}
|
|
||||||
if (message.forwardToContentScript) {
|
|
||||||
this.logger.log('info', 'comms', "[CommsServer.js::processReceivedMessage] Message has 'forward to content script' flag set. Forwarding message as is. Message:", message);
|
|
||||||
this.sendToFrame(message, message.targetTab, message.targetFrame);
|
|
||||||
}
|
|
||||||
if (message.forwardToAll) {
|
|
||||||
this.logger.log('info', 'comms', "[CommsServer.js::processReceivedMessage] Message has 'forward to all' flag set. Forwarding message as is. Message:", message);
|
|
||||||
this.sendToAll(message);
|
|
||||||
}
|
|
||||||
if (message.forwardToActive) {
|
|
||||||
this.logger.log('info', 'comms', "[CommsServer.js::processReceivedMessage] Message has 'forward to active' flag set. Forwarding message as is. Message:", message);
|
|
||||||
this.sendToActive(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async processReceivedMessage(message, port){
|
|
||||||
this.logger.log('info', 'comms', "[CommsServer.js::processReceivedMessage] Received message from popup/content script!", message, "port", port);
|
|
||||||
|
|
||||||
this.handleMessage(message, port)
|
|
||||||
}
|
|
||||||
|
|
||||||
processReceivedMessage_nonpersistent(message, sender){
|
|
||||||
this.logger.log('info', 'comms', "%c[CommsServer.js::processMessage_nonpersistent] Received message from background script!", "background-color: #11D; color: #aad", message, sender);
|
|
||||||
|
|
||||||
this.handleMessage(message, sender);
|
|
||||||
}
|
|
||||||
|
|
||||||
// chrome shitiness mitigation
|
|
||||||
sendUnmarkPlayer(message) {
|
|
||||||
this.logger.log('info', 'comms', '[CommsServer.js::sendUnmarkPlayer] Chrome is a shit browser that doesn\'t do port.postMessage() in unload events, so we have to resort to inelegant hacks. If you see this, then the workaround method works.');
|
|
||||||
this.processReceivedMessage(message, this.popupPort);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default CommsServer;
|
|
||||||
@ -1,129 +0,0 @@
|
|||||||
import BrowserDetect from '../../conf/BrowserDetect';
|
|
||||||
|
|
||||||
function url(file) {
|
|
||||||
return BrowserDetect.getURL(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class FontLoader {
|
|
||||||
static loadFonts() {
|
|
||||||
const fontsStyleElement = document.createElement('style');
|
|
||||||
fontsStyleElement.type = 'text/css';
|
|
||||||
|
|
||||||
fontsStyleElement.textContent = `
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Overpass';
|
|
||||||
src: ${url('res/fonts/overpass-webfont/overpass-thin.woff2')} format('woff2'); /* Super Modern Browsers */
|
|
||||||
font-weight: 200;
|
|
||||||
font-style: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Overpass';
|
|
||||||
src: ${url('res/fonts/overpass-webfont/overpass-thin-italic.woff2')} format('woff2');
|
|
||||||
font-weight: 200;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Overpass';
|
|
||||||
src: ${url('res/fonts/overpass-webfont/overpass-extralight.woff2')} format('woff2');
|
|
||||||
font-weight: 300;
|
|
||||||
font-style: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Overpass';
|
|
||||||
src: ${url('res/fonts/overpass-webfont/overpass-extralight-italic.woff2')} format('woff2');
|
|
||||||
font-weight: 300;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Overpass';
|
|
||||||
src: ${url('res/fonts/overpass-webfont/overpass-light.woff2')} format('woff2');
|
|
||||||
font-weight: 400;
|
|
||||||
font-style: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Overpass';
|
|
||||||
src: ${url('res/fonts/overpass-webfont/overpass-light-italic.woff2')} format('woff2');
|
|
||||||
font-weight: 400;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Overpass';
|
|
||||||
src: ${url('res/fonts/overpass-webfont/overpass-regular.woff2')} format('woff2');
|
|
||||||
font-weight: 500;
|
|
||||||
font-style: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Overpass';
|
|
||||||
src: ${url('res/fonts/overpass-webfont/overpass-italic.woff2')} format('woff2');
|
|
||||||
font-weight: 500;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Overpass';
|
|
||||||
src: ${url('res/fonts/overpass-webfont/overpass-semibold.woff2')} format('woff2');
|
|
||||||
font-weight: 600;
|
|
||||||
font-style: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Overpass';
|
|
||||||
src: ${url('res/fonts/overpass-webfont/overpass-semibold-italic.woff2')} format('woff2');
|
|
||||||
font-weight: 600;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Overpass';
|
|
||||||
src: ${url('res/fonts/overpass-webfont/overpass-bold.woff2')} format('woff2');
|
|
||||||
font-weight: 700;
|
|
||||||
font-style: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Overpass';
|
|
||||||
src: ${url('res/fonts/overpass-webfont/overpass-bold-italic.woff2')} format('woff2');
|
|
||||||
font-weight: 700;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Overpass mono';
|
|
||||||
src: ${url('res/fonts/overpass-mono-webfont/overpass-mono-light.woff2')} format('woff2');
|
|
||||||
font-weight: 300;
|
|
||||||
font-style: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Overpass mono';
|
|
||||||
src: ${url('res/fonts/overpass-mono-webfont/overpass-mono-regular.woff2')} format('woff2');
|
|
||||||
font-weight: 400;
|
|
||||||
font-style: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Overpass mono';
|
|
||||||
src: ${url('res/fonts/overpass-mono-webfont/overpass-mono-semibold.woff2')} format('woff2');
|
|
||||||
font-weight: 500;
|
|
||||||
font-style: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Overpass mono';
|
|
||||||
src: ${url('res/fonts/overpass-mono-webfont/overpass-mono-bold.woff2')} format('woff2');
|
|
||||||
font-weight: 600;
|
|
||||||
font-style: normal;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
document.head.appendChild(fontsStyleElement);
|
|
||||||
console.log("font loaded!")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,183 +0,0 @@
|
|||||||
import UI from './UI';
|
|
||||||
import VuexWebExtensions from 'vuex-webextensions';
|
|
||||||
import NotificationUi from '../../../csui/NotificationUi.vue';
|
|
||||||
import Notifications from '../../../common/data/notifications';
|
|
||||||
|
|
||||||
if (process.env.CHANNEL !== 'stable'){
|
|
||||||
console.info("Loading: PlayerNotificationUi");
|
|
||||||
}
|
|
||||||
|
|
||||||
let MuteScope = Object.freeze({
|
|
||||||
CurrentSite: 'current-site',
|
|
||||||
Global: 'global'
|
|
||||||
});
|
|
||||||
|
|
||||||
class PlayerNotificationUi extends UI {
|
|
||||||
|
|
||||||
constructor (
|
|
||||||
playerElement,
|
|
||||||
settings
|
|
||||||
) {
|
|
||||||
super(
|
|
||||||
'notification',
|
|
||||||
PlayerNotificationUi.getStoreConfig(),
|
|
||||||
PlayerNotificationUi.getUiConfig(playerElement),
|
|
||||||
PlayerNotificationUi.getCommsConfig()
|
|
||||||
);
|
|
||||||
|
|
||||||
this.settings = settings;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//#region constructor helpers
|
|
||||||
// we will move some things out of the constructor in order to keep things clean
|
|
||||||
static getStoreConfig() {
|
|
||||||
return {
|
|
||||||
plugins: [
|
|
||||||
VuexWebExtensions({
|
|
||||||
persistentStates: [
|
|
||||||
'notificationConfig'
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
state: {
|
|
||||||
// should be null by default!
|
|
||||||
notificationConfig: {
|
|
||||||
text: 'sample text <b>now with 100% more html formatting!</b>',
|
|
||||||
icon: 'exclamation-circle',
|
|
||||||
timeout: 5000,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mutations: {
|
|
||||||
'uw-set-notification'(state, payload) {
|
|
||||||
state['notificationConfig'] = payload;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
actions: {
|
|
||||||
'uw-set-notification'({commit}, payload) {
|
|
||||||
commit('uw-set-notification', payload);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
static getUiConfig(playerElement) {
|
|
||||||
return {
|
|
||||||
parentElement: playerElement,
|
|
||||||
component: NotificationUi
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static getCommsConfig() {
|
|
||||||
return {
|
|
||||||
handlers: {
|
|
||||||
'show-notification': [(message) => this.showNotification(message)],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//#endregion
|
|
||||||
|
|
||||||
//#region lifecycle
|
|
||||||
replace(playerElement) {
|
|
||||||
super.replace(this.getUiConfig(playerElement));
|
|
||||||
}
|
|
||||||
//#endregion
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show notification on screen.
|
|
||||||
*
|
|
||||||
* @param notificationConfig notification config (or ID of notification config in /common/data/notifications.js)
|
|
||||||
*
|
|
||||||
* notificationConfig should resemble this:
|
|
||||||
* {
|
|
||||||
* timeout: number — how long we'll be displaying the notification. If empty, 10s. -1: until user dismisses it
|
|
||||||
* icon: string — what icon we're gonna show. We're using bootstrap icons. Can be empty.
|
|
||||||
* text: — notification text. Supports HTML.
|
|
||||||
* notificationActions: [
|
|
||||||
* {
|
|
||||||
* command: function that gets executed upon clicking the button.
|
|
||||||
* label: label of the button
|
|
||||||
* icon: icon of the button
|
|
||||||
* }
|
|
||||||
* ],
|
|
||||||
* hideActions: [
|
|
||||||
* // more of notificationActions but with special case
|
|
||||||
* ]
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* When notificationConfig is a string, the function will add two additional notifications on the notificationActionsPile
|
|
||||||
* * never show this notification ever again on any site
|
|
||||||
* * never show this notification again on this site
|
|
||||||
*/
|
|
||||||
showNotification(notificationConfig) {
|
|
||||||
if (typeof notificationConfig === 'string') {
|
|
||||||
try {
|
|
||||||
const config = Notifications[notificationConfig];
|
|
||||||
|
|
||||||
// this should _never_ appear on production version of the extension, but it should help with development.
|
|
||||||
if (!config) {
|
|
||||||
return this.vuexStore?.dispatch('uw-set-notification', {
|
|
||||||
icon: 'x-circle-fill',
|
|
||||||
text: `Notification for key ${notificationConfig} does not exist.`,
|
|
||||||
timeout: -1,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// don't show notification if it's muted
|
|
||||||
if (this.isNotificationMuted(notificationConfig)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.vuexStore?.dispatch('uw-set-notification', {
|
|
||||||
...config,
|
|
||||||
hideActions: [
|
|
||||||
{
|
|
||||||
command: () => this.muteNotification(notificationConfig, MuteScope.CurrentSite),
|
|
||||||
label: 'this site'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
command: () => this.muteNotification(notificationConfig, MuteScope.Global),
|
|
||||||
label: 'never ever'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
console.error('theres been an error:', e)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.vuexStore?.dispatch('uw-set-notification', notificationConfig);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
muteNotification(notificationId, scope) {
|
|
||||||
// ensure objects we try to set exist
|
|
||||||
if (!this.settings.active.mutedNotifications) {
|
|
||||||
this.settings.active.mutedNotifications = {};
|
|
||||||
}
|
|
||||||
if (!this.settings.active.mutedNotifications[notificationId]) {
|
|
||||||
this.settings.active.mutedNotifications[notificationId] = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
// actually mute notification
|
|
||||||
if (scope === MuteScope.Global) {
|
|
||||||
this.settings.active.mutedNotifications[notificationId].$global = true;
|
|
||||||
} else {
|
|
||||||
this.settings.active.mutedNotifications[notificationId][window.location.hostname] = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// persist settings
|
|
||||||
this.settings.saveWithoutReload();
|
|
||||||
}
|
|
||||||
|
|
||||||
isNotificationMuted(notificationId) {
|
|
||||||
return this.settings.active.mutedNotifications?.[notificationId]?.$global
|
|
||||||
|| this.settings.active.mutedNotifications?.[notificationId]?.[window.location.hostname];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.env.CHANNEL !== 'stable'){
|
|
||||||
console.info("PlayerNotificationUi loaded");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export default PlayerNotificationUi;
|
|
||||||
@ -1,102 +0,0 @@
|
|||||||
import UI from './UI';
|
|
||||||
import VuexWebExtensions from 'vuex-webextensions';
|
|
||||||
import PlayerUiComponent from '../../../csui/PlayerUiComponent.vue';
|
|
||||||
|
|
||||||
if (process.env.CHANNEL !== 'stable'){
|
|
||||||
console.info("Loading: PlayerUi");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class that handles in-player UI
|
|
||||||
*/
|
|
||||||
class PlayerUi extends UI {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates new in-player UI for ultrawidify
|
|
||||||
* @param {*} playerElement PlayerUI will be created as a child of this element
|
|
||||||
* @param {*} settings Extension settings (instanceof Settings)
|
|
||||||
*/
|
|
||||||
constructor (
|
|
||||||
playerElement,
|
|
||||||
settings
|
|
||||||
) {
|
|
||||||
super(
|
|
||||||
'ultrawidifyUi',
|
|
||||||
PlayerUi.getStoreConfig(),
|
|
||||||
PlayerUi.getUiConfig(playerElement),
|
|
||||||
PlayerUi.getCommsConfig()
|
|
||||||
);
|
|
||||||
|
|
||||||
this.settings = settings;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//#region constructor helpers
|
|
||||||
// we will move some things out of the constructor in order to keep things clean
|
|
||||||
static getStoreConfig() {
|
|
||||||
// NOTE: these are sample values and can be deleted. Currently, they're commented out
|
|
||||||
// so we won't have to look up the documentation in order to get them working
|
|
||||||
return {
|
|
||||||
plugins: [
|
|
||||||
VuexWebExtensions({
|
|
||||||
persistentStates: [
|
|
||||||
'showUi'
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
state: {
|
|
||||||
showUi: true,
|
|
||||||
},
|
|
||||||
mutations: {
|
|
||||||
'uw-toggle-ui'(state) {
|
|
||||||
state['showUi'] = !state['showUi'];
|
|
||||||
},
|
|
||||||
'uw-set-ui-visible'(state, payload) {
|
|
||||||
state['showUi'] = payload;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
actions: {
|
|
||||||
'uw-set-ui-visible'({commit}, payload) {
|
|
||||||
commit('uw-set-ui-visible', payload);
|
|
||||||
},
|
|
||||||
'uw-toggle-ui'({commit}) {
|
|
||||||
commit('uw-toggle-ui');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
static getUiConfig(playerElement) {
|
|
||||||
return {
|
|
||||||
parentElement: playerElement,
|
|
||||||
component: PlayerUiComponent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static getCommsConfig() {
|
|
||||||
// NOTE: these are sample values and can be deleted. Currently, they're commented out
|
|
||||||
// so we won't have to look up the documentation in order to get them working
|
|
||||||
return {
|
|
||||||
handlers: {
|
|
||||||
// 'show-notification': [(message) => this.showNotification(message)],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//#endregion
|
|
||||||
|
|
||||||
//#region lifecycle
|
|
||||||
replace(playerElement) {
|
|
||||||
try {
|
|
||||||
super.replace(this.getUiConfig(playerElement));
|
|
||||||
} catch (e) {
|
|
||||||
this.logger.log('error', 'Couldn\'t replace player element for ui. Error:', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//#endregion
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.env.CHANNEL !== 'stable'){
|
|
||||||
console.info("PlayerUi loaded");
|
|
||||||
}
|
|
||||||
|
|
||||||
export default PlayerUi;
|
|
||||||
@ -1,87 +0,0 @@
|
|||||||
import { createApp } from 'vue';
|
|
||||||
import { createStore } from 'vuex';
|
|
||||||
|
|
||||||
if (process.env.CHANNEL !== 'stable'){
|
|
||||||
console.info("Loading: UI");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class UI {
|
|
||||||
constructor(
|
|
||||||
interfaceId,
|
|
||||||
storeConfig,
|
|
||||||
uiConfig, // {component, parentElement?}
|
|
||||||
commsConfig,
|
|
||||||
) {
|
|
||||||
this.interfaceId = interfaceId;
|
|
||||||
this.commsConfig = commsConfig;
|
|
||||||
this.storeConfig = storeConfig;
|
|
||||||
this.uiConfig = uiConfig;
|
|
||||||
|
|
||||||
this.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
async init() {
|
|
||||||
// initialize vuejs, but only once (check handled in initVue())
|
|
||||||
// we need to initialize this _after_ initializing comms.
|
|
||||||
|
|
||||||
this.initVue();
|
|
||||||
}
|
|
||||||
|
|
||||||
async initVue() {
|
|
||||||
if (this.storeConfig) {
|
|
||||||
this.vuexStore = createStore(this.storeConfig);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.initUi();
|
|
||||||
}
|
|
||||||
|
|
||||||
async initUi() {
|
|
||||||
const random = Math.round(Math.random() * 69420);
|
|
||||||
const uwid = `uw-${this.interfaceId}-root-${random}`
|
|
||||||
|
|
||||||
const rootDiv = document.createElement('div');
|
|
||||||
|
|
||||||
if (this.uiConfig.additionalStyle) {
|
|
||||||
rootDiv.setAttribute('style', this.uiConfig.additionalStyle);
|
|
||||||
}
|
|
||||||
rootDiv.setAttribute('id', uwid);
|
|
||||||
rootDiv.classList.add('uw-ultrawidify-container-root');
|
|
||||||
|
|
||||||
if (this.uiConfig?.parentElement) {
|
|
||||||
this.uiConfig.parentElement.appendChild(rootDiv);
|
|
||||||
} else {
|
|
||||||
document.body.appendChild(rootDiv);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.element = rootDiv;
|
|
||||||
|
|
||||||
const app = createApp(this.uiConfig.component);
|
|
||||||
if (this.vuexStore) {
|
|
||||||
app.use(this.vuexStore);
|
|
||||||
}
|
|
||||||
app.mount(`#${uwid}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Replaces ui config and re-inits the UI
|
|
||||||
* @param {*} newUiConfig
|
|
||||||
*/
|
|
||||||
replace(newUiConfig) {
|
|
||||||
this.element?.remove();
|
|
||||||
this.uiConfig = newUiConfig;
|
|
||||||
this.initUi();
|
|
||||||
}
|
|
||||||
|
|
||||||
destroy() {
|
|
||||||
// this.comms?.destroy();
|
|
||||||
this.element?.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.env.CHANNEL !== 'stable'){
|
|
||||||
console.info("UI.js loaded");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export default UI;
|
|
||||||
@ -1,658 +0,0 @@
|
|||||||
import Debug from '../../conf/Debug';
|
|
||||||
import VideoData from './VideoData';
|
|
||||||
import RescanReason from './enums/RescanReason.enum';
|
|
||||||
import AspectRatioType from '../../../common/enums/AspectRatioType.enum';
|
|
||||||
import CropModePersistence from '../../../common/enums/CropModePersistence.enum';
|
|
||||||
import Logger from '../Logger';
|
|
||||||
import Settings from '../Settings';
|
|
||||||
import ExtensionMode from '../../../common/enums/ExtensionMode.enum';
|
|
||||||
import CommsClient from '../comms/CommsClient';
|
|
||||||
|
|
||||||
if (process.env.CHANNEL !== 'stable'){
|
|
||||||
console.info("Loading PageInfo");
|
|
||||||
}
|
|
||||||
|
|
||||||
class PageInfo {
|
|
||||||
//#region flags
|
|
||||||
readOnly: boolean = false;
|
|
||||||
hasVideos: boolean = false;
|
|
||||||
siteDisabled: boolean = false;
|
|
||||||
//#endregion
|
|
||||||
|
|
||||||
//#region timers and timeouts
|
|
||||||
rescanTimer: any;
|
|
||||||
urlCheckTimer: any;
|
|
||||||
announceZoomTimeout: any;
|
|
||||||
//#endregion
|
|
||||||
|
|
||||||
//#region helper objects
|
|
||||||
logger: Logger;
|
|
||||||
settings: Settings;
|
|
||||||
comms: CommsClient;
|
|
||||||
videos: VideoData[] = [];
|
|
||||||
//#endregion
|
|
||||||
|
|
||||||
//#region misc stuff
|
|
||||||
lastUrl: string;
|
|
||||||
extensionMode: ExtensionMode;
|
|
||||||
defaultCrop: any;
|
|
||||||
currentCrop: any;
|
|
||||||
actionHandlerInitQueue: any[] = [];
|
|
||||||
currentZoomScale: number = 1;
|
|
||||||
|
|
||||||
actionHandler: any;
|
|
||||||
//#endregion
|
|
||||||
|
|
||||||
constructor(comms, settings, logger, extensionMode, readOnly = false){
|
|
||||||
this.logger = logger;
|
|
||||||
this.settings = settings;
|
|
||||||
|
|
||||||
this.lastUrl = window.location.href;
|
|
||||||
this.extensionMode = extensionMode;
|
|
||||||
this.readOnly = readOnly;
|
|
||||||
|
|
||||||
if (comms){
|
|
||||||
this.comms = comms;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// request inject css immediately
|
|
||||||
const playerStyleString = this.settings.active.sites[window.location.hostname].css.replace('\\n', '');
|
|
||||||
this.comms.sendMessage({
|
|
||||||
cmd: 'inject-css',
|
|
||||||
cssString: playerStyleString
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
// do nothing. It's ok if there's no special settings for the player element or crop persistence
|
|
||||||
}
|
|
||||||
|
|
||||||
// try getting default crop immediately.
|
|
||||||
// const cropModePersistence = this.settings.getDefaultCropPersistenceMode(window.location.hostname);
|
|
||||||
|
|
||||||
// try {
|
|
||||||
// if (cropModePersistence === CropModePersistence.Forever) {
|
|
||||||
// this.defaultCrop = this.settings.active.sites[window.location.hostname].defaultCrop;
|
|
||||||
// } else if (cropModePersistence === CropModePersistence.CurrentSession) {
|
|
||||||
// this.defaultCrop = JSON.parse(sessionStorage.getItem('uw-crop-mode-session-persistence'));
|
|
||||||
// }
|
|
||||||
// } catch (e) {
|
|
||||||
// // do nothing. It's ok if there's no special settings for the player element or crop persistence
|
|
||||||
// }
|
|
||||||
this.currentCrop = this.defaultCrop;
|
|
||||||
|
|
||||||
this.rescan(RescanReason.PERIODIC);
|
|
||||||
this.scheduleUrlCheck();
|
|
||||||
}
|
|
||||||
|
|
||||||
async injectCss(cssString) {
|
|
||||||
await this.comms.sendMessage({
|
|
||||||
cmd: 'inject-css',
|
|
||||||
cssString: cssString
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async ejectCss(cssString) {
|
|
||||||
await this.comms.sendMessage({
|
|
||||||
cmd: 'eject-css',
|
|
||||||
cssString: cssString
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async replaceCss(oldCssString, newCssString) {
|
|
||||||
await this.comms.sendMessage({
|
|
||||||
cmd: 'replace-css',
|
|
||||||
newCssString,
|
|
||||||
oldCssString
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
destroy() {
|
|
||||||
this.logger.log('info', ['debug', 'init'], "[PageInfo::destroy] destroying all videos!")
|
|
||||||
if(this.rescanTimer){
|
|
||||||
clearTimeout(this.rescanTimer);
|
|
||||||
}
|
|
||||||
for (let video of this.videos) {
|
|
||||||
try {
|
|
||||||
(this.comms.unregisterVideo as any)(video.vdid)
|
|
||||||
video.destroy();
|
|
||||||
} catch (e) {
|
|
||||||
this.logger.log('error', ['debug', 'init'], '[PageInfo::destroy] unable to destroy video! Error:', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const playerStyleString = this.settings.active.sites[window.location.hostname].css;
|
|
||||||
if (playerStyleString) {
|
|
||||||
this.comms.sendMessage({
|
|
||||||
cmd: 'eject-css',
|
|
||||||
cssString: playerStyleString
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// do nothing. It's ok if there's no special settings for the player element
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
reset() {
|
|
||||||
for(let video of this.videos) {
|
|
||||||
video.destroy();
|
|
||||||
}
|
|
||||||
this.rescan(RescanReason.MANUAL);
|
|
||||||
}
|
|
||||||
|
|
||||||
initMouseActionHandler(videoData) {
|
|
||||||
if (this.actionHandler) {
|
|
||||||
this.actionHandler.registerHandleMouse(videoData);
|
|
||||||
} else {
|
|
||||||
this.actionHandlerInitQueue.push(videoData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setActionHandler(actionHandler) {
|
|
||||||
this.actionHandler = actionHandler;
|
|
||||||
for (let item of this.actionHandlerInitQueue) {
|
|
||||||
this.actionHandler.registerHandleMouse(item);
|
|
||||||
}
|
|
||||||
this.actionHandlerInitQueue = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
getVideos(host) {
|
|
||||||
if (this.settings.active.sites[host]?.DOM?.video?.manual
|
|
||||||
&& this.settings.active.sites[host]?.DOM?.video?.querySelectors){
|
|
||||||
const videos = document.querySelectorAll(this.settings.active.sites[host].DOM.video.querySelectors);
|
|
||||||
|
|
||||||
if (videos.length) {
|
|
||||||
return videos;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return document.getElementsByTagName('video');
|
|
||||||
}
|
|
||||||
|
|
||||||
hasVideo() {
|
|
||||||
return this.readOnly ? this.hasVideos : this.videos.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
rescan(rescanReason){
|
|
||||||
const oldVideoCount = this.videos.length;
|
|
||||||
|
|
||||||
try{
|
|
||||||
let vids = this.getVideos(window.location.hostname);
|
|
||||||
|
|
||||||
if(!vids || vids.length == 0){
|
|
||||||
this.hasVideos = false;
|
|
||||||
|
|
||||||
if(rescanReason == RescanReason.PERIODIC){
|
|
||||||
this.logger.log('info', 'videoRescan', "[PageInfo::rescan] Scheduling normal rescan.")
|
|
||||||
this.scheduleRescan(RescanReason.PERIODIC);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// add new videos
|
|
||||||
this.hasVideos = false;
|
|
||||||
let videoExists = false;
|
|
||||||
let video, v;
|
|
||||||
|
|
||||||
for (video of vids) {
|
|
||||||
// če najdemo samo en video z višino in širino, to pomeni, da imamo na strani veljavne videe
|
|
||||||
// če trenutni video nima definiranih teh vrednostih, preskočimo vse nadaljnja preverjanja
|
|
||||||
// <===[:::::::]===>
|
|
||||||
// if we find even a single video with width and height, that means the page has valid videos
|
|
||||||
// if video lacks either of the two properties, we skip all further checks cos pointless
|
|
||||||
if(video.offsetWidth && video.offsetHeight){
|
|
||||||
this.hasVideos = true;
|
|
||||||
|
|
||||||
if (this.readOnly) {
|
|
||||||
// in lite mode, we're done. This is all the info we want, but we want to actually start doing
|
|
||||||
// things that interfere with the website. We still want to be running a rescan, tho.
|
|
||||||
|
|
||||||
if(rescanReason == RescanReason.PERIODIC){
|
|
||||||
this.scheduleRescan(RescanReason.PERIODIC);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
videoExists = false;
|
|
||||||
|
|
||||||
for (v of this.videos) {
|
|
||||||
if (v.destroyed) {
|
|
||||||
continue; //TODO: if destroyed video is same as current video, copy aspect ratio settings to current video
|
|
||||||
}
|
|
||||||
|
|
||||||
if (v.video == video) {
|
|
||||||
videoExists = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (videoExists) {
|
|
||||||
continue;
|
|
||||||
} else {
|
|
||||||
this.logger.log('info', 'videoRescan', "[PageInfo::rescan] found new video candidate:", video, "NOTE:: Video initialization starts here:\n--------------------------------\n")
|
|
||||||
|
|
||||||
try {
|
|
||||||
v = new VideoData(video, this.settings, this);
|
|
||||||
this.videos.push(v);
|
|
||||||
} catch (e) {
|
|
||||||
this.logger.log('error', 'debug', "rescan error: failed to initialize videoData. Skipping this video.",e);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.log('info', 'videoRescan', "END VIDEO INITIALIZATION\n\n\n-------------------------------------\nvideos[] is now this:", this.videos,"\n\n\n\n\n\n\n\n")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.removeDestroyed();
|
|
||||||
|
|
||||||
// če smo ostali brez videev, potem odregistriraj stran.
|
|
||||||
// če nismo ostali brez videev, potem registriraj stran.
|
|
||||||
//
|
|
||||||
// if we're left without videos on the current page, we unregister the page.
|
|
||||||
// if we have videos, we call register.
|
|
||||||
if (this.comms) {
|
|
||||||
// We used to send "register video" requests only on the first load, or if the number of
|
|
||||||
// videos on the page has changed. However, since Chrome Web Store started to require every
|
|
||||||
// extension requiring "broad permissions" to undergo manual review
|
|
||||||
// ... and since Chrome Web Store is known for taking their sweet ass time reviewing extensions,
|
|
||||||
// with review times north of an entire fucking month
|
|
||||||
// ... and since the legacy way of checking whether our frames-with-videos cache in background
|
|
||||||
// script contains any frames that no longer exist required us to use webNavigation.getFrame()/
|
|
||||||
// webNavigation.getAllFrames(), which requires a permission that triggers a review.
|
|
||||||
//
|
|
||||||
// While the extension uses some other permissions that trigger manual review, it's said that
|
|
||||||
// less is better / has a positive effect on your manual review times ... So I guess we'll do
|
|
||||||
// things in the less-than-optimal. more-than-retarded way.
|
|
||||||
//
|
|
||||||
// no but honestly fuck Chrome.
|
|
||||||
|
|
||||||
// if (this.videos.length != oldVideoCount) {
|
|
||||||
// }
|
|
||||||
|
|
||||||
if (this.videos.length > 0) {
|
|
||||||
// this.comms.registerVideo({host: window.location.hostname, location: window.location});
|
|
||||||
this.comms.registerVideo();
|
|
||||||
} else {
|
|
||||||
// this.comms.unregisterVideo({host: window.location.hostname, location: window.location});
|
|
||||||
this.comms.unregisterVideo();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch(e) {
|
|
||||||
// če pride do zajeba, potem lahko domnevamo da na strani ni nobenega videa. Uničimo vse objekte videoData
|
|
||||||
// da preprečimo večkratno inicializacijo. Če smo se z našim ugibom zmotili, potem se bodo vsi videi ponovno
|
|
||||||
// našli ob naslednjem preiskovanju
|
|
||||||
//
|
|
||||||
// if we encounter a fuckup, we can assume that no videos were found on the page. We destroy all videoData
|
|
||||||
// objects to prevent multiple initialization (which happened, but I don't know why). No biggie if we destroyed
|
|
||||||
// videoData objects in error — they'll be back in the next rescan
|
|
||||||
this.logger.log('error', 'debug', "rescan error: — destroying all videoData objects",e);
|
|
||||||
for (const v of this.videos) {
|
|
||||||
v.destroy();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(rescanReason == RescanReason.PERIODIC){
|
|
||||||
this.scheduleRescan(RescanReason.PERIODIC);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
removeDestroyed(){
|
|
||||||
this.videos = this.videos.filter( vid => vid.destroyed === false);
|
|
||||||
}
|
|
||||||
|
|
||||||
scheduleRescan(rescanReason){
|
|
||||||
if(rescanReason != RescanReason.PERIODIC){
|
|
||||||
this.rescan(rescanReason);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try{
|
|
||||||
if(this.rescanTimer){
|
|
||||||
clearTimeout(this.rescanTimer);
|
|
||||||
}
|
|
||||||
|
|
||||||
let ths = this;
|
|
||||||
|
|
||||||
this.rescanTimer = setTimeout(function(rescanReason){
|
|
||||||
ths.rescanTimer = null;
|
|
||||||
ths.rescan(rescanReason);
|
|
||||||
ths = null;
|
|
||||||
}, this.settings.active.pageInfo.timeouts.rescan, RescanReason.PERIODIC)
|
|
||||||
} catch(e) {
|
|
||||||
this.logger.log('error', 'debug', "[PageInfo::scheduleRescan] scheduling rescan failed. Here's why:",e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
scheduleUrlCheck() {
|
|
||||||
try{
|
|
||||||
if(this.urlCheckTimer){
|
|
||||||
clearTimeout(this.urlCheckTimer);
|
|
||||||
}
|
|
||||||
|
|
||||||
let ths = this;
|
|
||||||
|
|
||||||
this.urlCheckTimer = setTimeout(function(){
|
|
||||||
ths.urlCheckTimer = null;
|
|
||||||
ths.ghettoUrlCheck();
|
|
||||||
ths = null;
|
|
||||||
}, this.settings.active.pageInfo.timeouts.urlCheck)
|
|
||||||
} catch(e){
|
|
||||||
this.logger.log('error', 'debug', "[PageInfo::scheduleUrlCheck] scheduling URL check failed. Here's why:",e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ghettoUrlCheck() {
|
|
||||||
if (this.lastUrl != window.location.href){
|
|
||||||
this.logger.log('error', 'videoRescan', "[PageInfo::ghettoUrlCheck] URL has changed. Triggering a rescan!");
|
|
||||||
|
|
||||||
this.rescan(RescanReason.URL_CHANGE);
|
|
||||||
this.lastUrl = window.location.href;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.scheduleUrlCheck();
|
|
||||||
}
|
|
||||||
|
|
||||||
initArDetection(playingOnly){
|
|
||||||
if (playingOnly) {
|
|
||||||
for(let vd of this.videos){
|
|
||||||
if(vd.isPlaying()) {
|
|
||||||
vd.initArDetection();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
for(let vd of this.videos){
|
|
||||||
vd.initArDetection();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// to je treba klicat ob menjavi zavihkov
|
|
||||||
// these need to be called on tab switch
|
|
||||||
pauseProcessing(playingOnly){
|
|
||||||
if (playingOnly) {
|
|
||||||
for(let vd of this.videos){
|
|
||||||
if (vd.isPlaying()) {
|
|
||||||
vd.pause();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for(let vd of this.videos){
|
|
||||||
vd.pause();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resumeProcessing(resumeAutoar = false, playingOnly = false){
|
|
||||||
if (playingOnly) {
|
|
||||||
for(let vd of this.videos){
|
|
||||||
if (vd.isPlaying()) {
|
|
||||||
vd.resume();
|
|
||||||
if(resumeAutoar){
|
|
||||||
vd.resumeAutoAr();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for(let vd of this.videos){
|
|
||||||
vd.resume();
|
|
||||||
if(resumeAutoar){
|
|
||||||
vd.resumeAutoAr();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
startArDetection(playingOnly){
|
|
||||||
if (Debug.debug) {
|
|
||||||
this.logger.log('info', 'debug', '[PageInfo::startArDetection()] starting automatic ar detection!')
|
|
||||||
}
|
|
||||||
if (playingOnly) {
|
|
||||||
for(let vd of this.videos){
|
|
||||||
if (vd.isPlaying()) {
|
|
||||||
vd.startArDetection();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for(let vd of this.videos){
|
|
||||||
vd.startArDetection();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stopArDetection(playingOnly){
|
|
||||||
if (playingOnly) {
|
|
||||||
for(let vd of this.videos){
|
|
||||||
if (vd.isPlaying()) {
|
|
||||||
vd.stopArDetection();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for(let vd of this.videos){
|
|
||||||
vd.stopArDetection();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setAr(ar, playingOnly?: boolean){
|
|
||||||
this.logger.log('info', 'debug', '[PageInfo::setAr] aspect ratio:', ar, "playing only?", playingOnly)
|
|
||||||
|
|
||||||
if (ar.type !== AspectRatioType.Automatic) {
|
|
||||||
this.stopArDetection(playingOnly);
|
|
||||||
} else {
|
|
||||||
this.logger.log('info', 'debug', '[PageInfo::setAr] aspect ratio is auto');
|
|
||||||
|
|
||||||
try {
|
|
||||||
for (let vd of this.videos) {
|
|
||||||
if (!playingOnly || vd.isPlaying()) {
|
|
||||||
vd.resetLastAr();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
this.logger.log('error', 'debug', "???", e);
|
|
||||||
}
|
|
||||||
this.initArDetection(playingOnly);
|
|
||||||
this.startArDetection(playingOnly);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: find a way to only change aspect ratio for one video
|
|
||||||
if (ar === AspectRatioType.Reset) {
|
|
||||||
for (let vd of this.videos) {
|
|
||||||
if (!playingOnly || vd.isPlaying()) {
|
|
||||||
vd.resetAr();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for (let vd of this.videos) {
|
|
||||||
if (!playingOnly || vd.isPlaying()) {
|
|
||||||
vd.setAr(ar)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setVideoAlignment(videoAlignment, playingOnly) {
|
|
||||||
if (playingOnly) {
|
|
||||||
for(let vd of this.videos) {
|
|
||||||
if (vd.isPlaying()) {
|
|
||||||
vd.setVideoAlignment(videoAlignment)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for(let vd of this.videos) {
|
|
||||||
vd.setVideoAlignment(videoAlignment)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setPanMode(mode, playingOnly?: boolean) {
|
|
||||||
if (playingOnly) {
|
|
||||||
for(let vd of this.videos) {
|
|
||||||
if (vd.isPlaying()) {
|
|
||||||
vd.setPanMode(mode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for(let vd of this.videos) {
|
|
||||||
vd.setPanMode(mode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
restoreAr(playingOnly?: boolean) {
|
|
||||||
if (playingOnly) {
|
|
||||||
for(let vd of this.videos){
|
|
||||||
if (vd.isPlaying()) {
|
|
||||||
vd.restoreAr();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for(let vd of this.videos){
|
|
||||||
vd.restoreAr();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setStretchMode(stretchMode, playingOnly?: boolean, fixedStretchRatio?: boolean){
|
|
||||||
// TODO: find a way to only change aspect ratio for one video
|
|
||||||
|
|
||||||
if (playingOnly) {
|
|
||||||
for(let vd of this.videos){
|
|
||||||
if (vd.isPlaying()) {
|
|
||||||
vd.setStretchMode(stretchMode, fixedStretchRatio)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for(let vd of this.videos){
|
|
||||||
vd.setStretchMode(stretchMode, fixedStretchRatio)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setZoom(zoomLevel, no_announce?: boolean, playingOnly?: boolean) {
|
|
||||||
if (playingOnly) {
|
|
||||||
for(let vd of this.videos) {
|
|
||||||
if (vd.isPlaying()) {
|
|
||||||
vd.setZoom(zoomLevel, no_announce);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for(let vd of this.videos) {
|
|
||||||
vd.setZoom(zoomLevel, no_announce);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
zoomStep(step, playingOnly?: boolean) {
|
|
||||||
for(let vd of this.videos){
|
|
||||||
if (!playingOnly || vd.isPlaying()) {
|
|
||||||
vd.zoomStep(step);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
markPlayer(name, color) {
|
|
||||||
for (let vd of this.videos) {
|
|
||||||
vd.markPlayer(name,color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
unmarkPlayer() {
|
|
||||||
for (let vd of this.videos) {
|
|
||||||
vd.unmarkPlayer();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
announceZoom(scale) {
|
|
||||||
if (this.announceZoomTimeout) {
|
|
||||||
clearTimeout(this.announceZoomTimeout);
|
|
||||||
}
|
|
||||||
this.currentZoomScale = scale;
|
|
||||||
const ths = this;
|
|
||||||
this.announceZoomTimeout = setTimeout(() => ths.comms.announceZoom(scale), this.settings.active.zoom.announceDebounce);
|
|
||||||
}
|
|
||||||
|
|
||||||
setManualTick(manualTick) {
|
|
||||||
for(let vd of this.videos) {
|
|
||||||
vd.setManualTick(manualTick);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tick() {
|
|
||||||
for(let vd of this.videos) {
|
|
||||||
vd.tick();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sendPerformanceUpdate(performanceUpdate) {
|
|
||||||
if(this.comms) {
|
|
||||||
this.comms.sendPerformanceUpdate(performanceUpdate);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
requestCurrentZoom() {
|
|
||||||
this.comms.announceZoom(this.currentZoomScale);
|
|
||||||
}
|
|
||||||
|
|
||||||
setKeyboardShortcutsEnabled(state) {
|
|
||||||
this.actionHandler.setKeyboardLocal(state);
|
|
||||||
}
|
|
||||||
|
|
||||||
setArPersistence(persistenceMode) {
|
|
||||||
// name of this function is mildly misleading — we don't really _set_ ar persistence. (Ar persistence
|
|
||||||
// mode is set and saved via popup or keyboard shortcuts, if user defined them) We just save the current
|
|
||||||
// aspect ratio whenever aspect ratio persistence mode changes.
|
|
||||||
if (persistenceMode === CropModePersistence.CurrentSession) {
|
|
||||||
sessionStorage.setItem('uw-crop-mode-session-persistence', JSON.stringify(this.currentCrop));
|
|
||||||
} else if (persistenceMode === CropModePersistence.Forever) {
|
|
||||||
if (this.settings.active.sites[window.location.hostname]) {
|
|
||||||
// | key may be missing, so we do this
|
|
||||||
this.settings.active.sites[window.location.hostname]['defaultAr'] = this.currentCrop;
|
|
||||||
} else {
|
|
||||||
this.settings.active.sites[window.location.hostname] = this.settings.getDefaultOption();
|
|
||||||
this.settings.active.sites[window.location.hostname]['defaultAr'] = this.currentCrop;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.settings.saveWithoutReload();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateCurrentCrop(ar) {
|
|
||||||
this.currentCrop = ar;
|
|
||||||
// This means crop persistance is disabled. If crop persistance is enabled, then settings for current
|
|
||||||
// site MUST exist (crop persistence mode is disabled by default)
|
|
||||||
|
|
||||||
const cropModePersistence = this.settings.getDefaultCropPersistenceMode(window.location.hostname);
|
|
||||||
|
|
||||||
if (cropModePersistence === CropModePersistence.Disabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.defaultCrop = ar;
|
|
||||||
|
|
||||||
if (cropModePersistence === CropModePersistence.CurrentSession) {
|
|
||||||
sessionStorage.setItem('uw-crop-mode-session-persistence', JSON.stringify(ar));
|
|
||||||
} else if (cropModePersistence === CropModePersistence.Forever) {
|
|
||||||
if (this.settings.active.sites[window.location.hostname]) {
|
|
||||||
// | key may be missing, so we do this
|
|
||||||
this.settings.active.sites[window.location.hostname]['defaultAr'] = ar;
|
|
||||||
} else {
|
|
||||||
this.settings.active.sites[window.location.hostname] = this.settings.getDefaultOption();
|
|
||||||
this.settings.active.sites[window.location.hostname]['defaultAr'] = ar;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.settings.saveWithoutReload();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.env.CHANNEL !== 'stable'){
|
|
||||||
console.info("PageInfo loaded!");
|
|
||||||
}
|
|
||||||
|
|
||||||
export default PageInfo;
|
|
||||||
@ -1,583 +0,0 @@
|
|||||||
import Debug from '../../conf/Debug';
|
|
||||||
import ExtensionMode from '../../../common/enums/ExtensionMode.enum'
|
|
||||||
import AspectRatioType from '../../../common/enums/AspectRatioType.enum';
|
|
||||||
import PlayerNotificationUi from '../uwui/PlayerNotificationUI';
|
|
||||||
import PlayerUi from '../uwui/PlayerUI';
|
|
||||||
import BrowserDetect from '../../conf/BrowserDetect';
|
|
||||||
import * as _ from 'lodash';
|
|
||||||
import { sleep } from '../../../common/js/utils';
|
|
||||||
import VideoData from './VideoData';
|
|
||||||
import Settings from '../Settings';
|
|
||||||
import Logger from '../Logger';
|
|
||||||
|
|
||||||
if (process.env.CHANNEL !== 'stable'){
|
|
||||||
console.info("Loading: PlayerData.js");
|
|
||||||
}
|
|
||||||
|
|
||||||
/* sprejme <video> tag (element) in seznam imen, ki se lahko pojavijo v razredih oz. id staršev.
|
|
||||||
// vrne dimenzije predvajalnika (širina, višina)
|
|
||||||
//
|
|
||||||
// Na youtube v theater mode je razširitev rahlo pokvarjena. Video tag ostane večji od predvajalnika, ko se zapusti
|
|
||||||
// celozaslonski način. Ta funkcija skuša to težavo rešiti tako, da poišče element predvajalnika, ki je zavit okoli videa.
|
|
||||||
//
|
|
||||||
// Funkcija izkorišča lastnost, da bi načeloma moral biti vsak zunanji element večji od notranjega. Najmanjši element od
|
|
||||||
// <video> značke pa do korena drevesa bi tako moral biti predvajalnik.
|
|
||||||
//
|
|
||||||
// Če je podan seznam imen, potem funkcija vrne dimenzije prvega elementa, ki v id oz. razredu vsebuje katerokoli ime iz seznama
|
|
||||||
//
|
|
||||||
// | EN |
|
|
||||||
//
|
|
||||||
// accepts <video> tag (element) and list of names that can appear in id or class
|
|
||||||
// returns player dimensions (width, height)
|
|
||||||
//
|
|
||||||
// Theater mode is mildly broken on youtube. <video> tag remains bigger than the player after leaving the fullscreen mode, and
|
|
||||||
// there's nothing we can do about that. This function aims to solve the problem by finding the player element that's wrapped around
|
|
||||||
// the <video> tag.
|
|
||||||
//
|
|
||||||
// In general, an outer tag should be bigger than the inner tag. Therefore the smallest element between <video> tag and the document
|
|
||||||
// root should be the player.
|
|
||||||
//
|
|
||||||
// If list of names is provided, the function returns dimensions of the first element that contains any name from the list in either
|
|
||||||
// id or class.
|
|
||||||
*/
|
|
||||||
|
|
||||||
class PlayerData {
|
|
||||||
|
|
||||||
//#region helper objects
|
|
||||||
logger: Logger;
|
|
||||||
videoData: VideoData;
|
|
||||||
settings: Settings;
|
|
||||||
notificationService: PlayerNotificationUi;
|
|
||||||
//#endregion
|
|
||||||
|
|
||||||
//#region HTML objects
|
|
||||||
video: any;
|
|
||||||
element: any;
|
|
||||||
overlayNode: any;
|
|
||||||
//#endregion
|
|
||||||
|
|
||||||
//#region flags
|
|
||||||
invalid: boolean = false;
|
|
||||||
private periodicallyRefreshPlayerElement: boolean = false;
|
|
||||||
halted: boolean = true;
|
|
||||||
|
|
||||||
//#region misc stuff
|
|
||||||
extensionMode: any;
|
|
||||||
dimensions: {width?: number, height?: number, fullscreen?: boolean};
|
|
||||||
private playerIdElement: any;
|
|
||||||
private observer: ResizeObserver;
|
|
||||||
//#endregion
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets player aspect ratio. If in full screen, it returns screen aspect ratio unless settings say otherwise.
|
|
||||||
*/
|
|
||||||
get aspectRatio() {
|
|
||||||
try {
|
|
||||||
if (this.dimensions?.fullscreen && !this.settings.getSettingsForSite()?.usePlayerArInFullscreen) {
|
|
||||||
return window.innerWidth / window.innerHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.dimensions.width / this.dimensions.height;
|
|
||||||
} catch (e) {
|
|
||||||
console.error('cannot determine aspect ratio!', e);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(videoData) {
|
|
||||||
try {
|
|
||||||
this.logger = videoData.logger;
|
|
||||||
this.videoData = videoData;
|
|
||||||
this.video = videoData.video;
|
|
||||||
this.settings = videoData.settings;
|
|
||||||
this.extensionMode = videoData.extensionMode;
|
|
||||||
this.invalid = false;
|
|
||||||
this.element = this.getPlayer();
|
|
||||||
|
|
||||||
this.notificationService = new PlayerNotificationUi(this.element, this.settings);
|
|
||||||
|
|
||||||
this.dimensions = undefined;
|
|
||||||
this.overlayNode = undefined;
|
|
||||||
|
|
||||||
this.periodicallyRefreshPlayerElement = false;
|
|
||||||
try {
|
|
||||||
this.periodicallyRefreshPlayerElement = this.settings.active.sites[window.location.hostname].DOM.player.periodicallyRefreshPlayerElement;
|
|
||||||
} catch (e) {
|
|
||||||
// no biggie — that means we don't have any special settings for this site.
|
|
||||||
}
|
|
||||||
|
|
||||||
// this happens when we don't find a matching player element
|
|
||||||
if (!this.element) {
|
|
||||||
this.invalid = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.extensionMode === ExtensionMode.Enabled) {
|
|
||||||
this.checkPlayerSizeChange();
|
|
||||||
}
|
|
||||||
this.startChangeDetection();
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
console.error('[Ultrawidify::PlayerData::ctor] There was an error setting up player data. You should be never seeing this message. Error:', e);
|
|
||||||
this.invalid = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns whether we're in fullscreen mode or not.
|
|
||||||
*/
|
|
||||||
static isFullScreen(){
|
|
||||||
const ihdiff = Math.abs(window.screen.height - window.innerHeight);
|
|
||||||
const iwdiff = Math.abs(window.screen.width - window.innerWidth);
|
|
||||||
|
|
||||||
// Chrome on linux on X on mixed PPI displays may return ever so slightly different values
|
|
||||||
// for innerHeight vs screen.height abd innerWidth vs. screen.width, probably courtesy of
|
|
||||||
// fractional scaling or something. This means we'll give ourself a few px of margin — the
|
|
||||||
// window elements visible in not-fullscreen are usually double digit px tall
|
|
||||||
return ( ihdiff < 5 && iwdiff < 5 );
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
onPlayerDimensionsChanged(mutationList?, observer?) {
|
|
||||||
if (this?.checkPlayerSizeChange()) {
|
|
||||||
this.videoData.resizer.restore();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
start(){
|
|
||||||
this.startChangeDetection();
|
|
||||||
}
|
|
||||||
|
|
||||||
stop(){
|
|
||||||
this.halted = true;
|
|
||||||
this.stopChangeDetection();
|
|
||||||
}
|
|
||||||
|
|
||||||
destroy() {
|
|
||||||
this.stopChangeDetection();
|
|
||||||
this.destroyOverlay();
|
|
||||||
this.notificationService?.destroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
startChangeDetection(){
|
|
||||||
if (this.invalid) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (BrowserDetect.firefox) {
|
|
||||||
this.observer = new ResizeObserver(
|
|
||||||
_.debounce( // don't do this too much:
|
|
||||||
this.onPlayerDimensionsChanged,
|
|
||||||
250, // do it once per this many ms
|
|
||||||
{
|
|
||||||
leading: true, // do it when we call this fallback first
|
|
||||||
trailing: true // do it after the timeout if we call this callback few more times
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// Chrome for some reason insists that this.onPlayerDimensionsChanged is not a function
|
|
||||||
// when it's not wrapped into an anonymous function
|
|
||||||
this.observer = new ResizeObserver(
|
|
||||||
_.debounce( // don't do this too much:
|
|
||||||
(m,o) => this.onPlayerDimensionsChanged(m,o),
|
|
||||||
250, // do it once per this many ms
|
|
||||||
{
|
|
||||||
leading: true, // do it when we call this fallback first
|
|
||||||
trailing: true // do it after the timeout if we call this callback few more times
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const observerConf = {
|
|
||||||
attributes: true,
|
|
||||||
// attributeFilter: ['style', 'class'],
|
|
||||||
attributeOldValue: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.observer.observe(this.element);
|
|
||||||
} catch (e) {
|
|
||||||
console.error("failed to set observer",e )
|
|
||||||
}
|
|
||||||
// legacy mode still exists, but acts as a fallback for observers and is triggered less
|
|
||||||
// frequently in order to avoid too many pointless checks
|
|
||||||
this.legacyChangeDetection();
|
|
||||||
}
|
|
||||||
|
|
||||||
async legacyChangeDetection() {
|
|
||||||
while (!this.halted) {
|
|
||||||
await sleep(1000);
|
|
||||||
try {
|
|
||||||
this.doPeriodicPlayerElementChangeCheck();
|
|
||||||
} catch (e) {
|
|
||||||
console.error('[PlayerData::legacycd] this message is pretty high on the list of messages you shouldn\'t see', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
doPeriodicPlayerElementChangeCheck() {
|
|
||||||
if (this.periodicallyRefreshPlayerElement) {
|
|
||||||
this.forceDetectPlayerElementChange();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stopChangeDetection(){
|
|
||||||
this.observer.disconnect();
|
|
||||||
}
|
|
||||||
|
|
||||||
makeOverlay() {
|
|
||||||
if (!this.overlayNode) {
|
|
||||||
this.destroyOverlay();
|
|
||||||
}
|
|
||||||
|
|
||||||
let overlay = document.createElement('div');
|
|
||||||
overlay.style.width = '100%';
|
|
||||||
overlay.style.height = '100%';
|
|
||||||
overlay.style.position = 'absolute';
|
|
||||||
overlay.style.top = '0';
|
|
||||||
overlay.style.left = '0';
|
|
||||||
overlay.style.zIndex = '1000000000';
|
|
||||||
overlay.style.pointerEvents = 'none';
|
|
||||||
|
|
||||||
this.overlayNode = overlay;
|
|
||||||
this.element.appendChild(overlay);
|
|
||||||
}
|
|
||||||
|
|
||||||
destroyOverlay() {
|
|
||||||
if(this.playerIdElement) {
|
|
||||||
this.playerIdElement.remove();
|
|
||||||
this.playerIdElement = undefined;
|
|
||||||
}
|
|
||||||
if (this.overlayNode) {
|
|
||||||
this.overlayNode.remove();
|
|
||||||
this.overlayNode = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
markPlayer(name, color) {
|
|
||||||
if (!this.overlayNode) {
|
|
||||||
this.makeOverlay();
|
|
||||||
}
|
|
||||||
if (this.playerIdElement) {
|
|
||||||
this.playerIdElement.remove();
|
|
||||||
}
|
|
||||||
this.playerIdElement = document.createElement('div');
|
|
||||||
this.playerIdElement.innerHTML = `<div style="background-color: ${color}; color: #fff; position: absolute; top: 0; left: 0">${name}</div>`;
|
|
||||||
|
|
||||||
this.overlayNode.appendChild(this.playerIdElement);
|
|
||||||
}
|
|
||||||
|
|
||||||
unmarkPlayer() {
|
|
||||||
this.logger.log('info', 'debug', "[PlayerData::unmarkPlayer] unmarking player!", {playerIdElement: this.playerIdElement});
|
|
||||||
if (this.playerIdElement) {
|
|
||||||
this.playerIdElement.innerHTML = '';
|
|
||||||
this.playerIdElement.remove();
|
|
||||||
}
|
|
||||||
this.playerIdElement = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
collectionHas(collection, element) {
|
|
||||||
for (let i = 0, len = collection.length; i < len; i++) {
|
|
||||||
if (collection[i] == element) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
updatePlayerDimensions(element) {
|
|
||||||
const isFullScreen = PlayerData.isFullScreen();
|
|
||||||
|
|
||||||
if (element.offsetWidth !== this.dimensions?.width
|
|
||||||
|| element.offsetHeight !== this.dimensions?.height
|
|
||||||
|| isFullScreen !== this.dimensions?.fullscreen) {
|
|
||||||
|
|
||||||
// update dimensions only if they've changed, _before_ we do a restore (not after)
|
|
||||||
this.dimensions = {
|
|
||||||
width: element.offsetWidth,
|
|
||||||
height: element.offsetHeight,
|
|
||||||
fullscreen: isFullScreen
|
|
||||||
};
|
|
||||||
|
|
||||||
// actually re-calculate zoom when player size changes, but only if videoData.resizer
|
|
||||||
// is defined. Since resizer needs a PlayerData object to exist, videoData.resizer will
|
|
||||||
// be undefined the first time this function will run.
|
|
||||||
this.videoData.resizer?.restore();
|
|
||||||
|
|
||||||
// NOTE: it's possible that notificationService hasn't been initialized yet at this point.
|
|
||||||
// no biggie if it wasn't, we just won't replace the notification UI
|
|
||||||
this.notificationService?.replace(this.element);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getPlayer() {
|
|
||||||
const host = window.location.hostname;
|
|
||||||
let element = this.video.parentNode;
|
|
||||||
const videoWidth = this.video.offsetWidth;
|
|
||||||
const videoHeight = this.video.offsetHeight;
|
|
||||||
const elementQ = [];
|
|
||||||
const scorePenalty = 10;
|
|
||||||
const sizePenaltyMultiplier = 0.1;
|
|
||||||
let penaltyMultiplier = 0;
|
|
||||||
let score;
|
|
||||||
|
|
||||||
try {
|
|
||||||
if(! element ){
|
|
||||||
this.logger.log('info', 'debug', "[PlayerDetect::_pd_getPlayer] element is not valid, doing nothing.", element)
|
|
||||||
if(this.element) {
|
|
||||||
const ths = this;
|
|
||||||
}
|
|
||||||
this.element = undefined;
|
|
||||||
this.dimensions = undefined;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// log the entire hierarchy from <video> to root
|
|
||||||
if (this.logger.canLog('playerDetect')) {
|
|
||||||
const logObj = [];
|
|
||||||
logObj.push(`window size: ${window.innerWidth} x ${window.innerHeight}`);
|
|
||||||
let e = element;
|
|
||||||
while (e) {
|
|
||||||
logObj.push({offsetSize: {width: e.offsetWidth, height: e.offsetHeight}, clientSize: {width: e.clientWidth, height: e.clientHeight}, element: e});
|
|
||||||
e = e.parentNode;
|
|
||||||
}
|
|
||||||
this.logger.log('info', 'playerDetect', "\n\n[PlayerDetect::getPlayer()] element hierarchy (video->root)", logObj);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.settings.active.sites[host]?.DOM?.player?.manual) {
|
|
||||||
if (this.settings.active.sites[host]?.DOM?.player?.useRelativeAncestor
|
|
||||||
&& this.settings.active.sites[host]?.DOM?.player?.videoAncestor) {
|
|
||||||
|
|
||||||
let parentsLeft = this.settings.active.sites[host].DOM.player.videoAncestor - 1;
|
|
||||||
while (parentsLeft --> 0) {
|
|
||||||
element = element.parentNode;
|
|
||||||
}
|
|
||||||
if (element) {
|
|
||||||
this.updatePlayerDimensions(element);
|
|
||||||
return element;
|
|
||||||
}
|
|
||||||
} else if (this.settings.active.sites[host]?.DOM?.player?.querySelectors) {
|
|
||||||
const allSelectors = document.querySelectorAll(this.settings.active.sites[host].DOM.player.querySelectors);
|
|
||||||
// actually we'll also score this branch in a similar way we score the regular, auto branch
|
|
||||||
while (element) {
|
|
||||||
|
|
||||||
// Let's see how this works
|
|
||||||
if (this.collectionHas(allSelectors, element)) {
|
|
||||||
score = 100; // every matching element gets a baseline 100 points
|
|
||||||
|
|
||||||
// elements that match the size get a hefty bonus
|
|
||||||
if ( (element.offsetWidth >= videoWidth && this.equalish(element.offsetHeight, videoHeight, 2))
|
|
||||||
|| (element.offsetHeight >= videoHeight && this.equalish(element.offsetWidth, videoHeight, 2))) {
|
|
||||||
score += 75;
|
|
||||||
}
|
|
||||||
|
|
||||||
// elements farther away from the video get a penalty
|
|
||||||
score -= (scorePenalty) * 20;
|
|
||||||
|
|
||||||
// push the element on the queue/stack:
|
|
||||||
elementQ.push({
|
|
||||||
score: score,
|
|
||||||
element: element,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
element = element.parentNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
// log player candidates
|
|
||||||
this.logger.log('info', 'playerDetect', 'player detect via query selector: element queue and final element:', {queue: elementQ, bestCandidate: elementQ.length ? elementQ.sort( (a,b) => b.score - a.score)[0].element : 'n/a'});
|
|
||||||
|
|
||||||
if (elementQ.length) {
|
|
||||||
// return element with biggest score
|
|
||||||
// if video player has not been found, proceed to automatic detection
|
|
||||||
const playerElement = elementQ.sort( (a,b) => b.score - a.score)[0].element;
|
|
||||||
this.updatePlayerDimensions(playerElement);
|
|
||||||
return playerElement;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// try to find element the old fashioned way
|
|
||||||
|
|
||||||
while (element){
|
|
||||||
// odstranimo čudne elemente, ti bi pokvarili zadeve
|
|
||||||
// remove weird elements, those would break our stuff
|
|
||||||
if ( element.offsetWidth == 0 || element.offsetHeight == 0){
|
|
||||||
element = element.parentNode;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// element is player, if at least one of the sides is as long as the video
|
|
||||||
// note that we can't make any additional assumptions with regards to player
|
|
||||||
// size, since there are both cases where the other side is bigger _and_ cases
|
|
||||||
// where other side is smaller than the video.
|
|
||||||
//
|
|
||||||
// Don't bother thinking about this too much, as any "thinking" was quickly
|
|
||||||
// corrected by bugs caused by various edge cases.
|
|
||||||
if (
|
|
||||||
this.equalish(element.offsetHeight, videoHeight, 5)
|
|
||||||
|| this.equalish(element.offsetWidth, videoWidth, 5)
|
|
||||||
) {
|
|
||||||
score = 1000;
|
|
||||||
|
|
||||||
// -------------------
|
|
||||||
// PENALTIES
|
|
||||||
// -------------------
|
|
||||||
//
|
|
||||||
// Our ideal player will be as close to the video element, and it will als
|
|
||||||
// be as close to the size of the video.
|
|
||||||
|
|
||||||
// prefer elements closer to <video>
|
|
||||||
score -= scorePenalty * penaltyMultiplier++;
|
|
||||||
|
|
||||||
// the bigger the size difference between the video and the player,
|
|
||||||
// the more penalty we'll incur. Since we did some grace ith
|
|
||||||
let playerSizePenalty = 1;
|
|
||||||
if ( element.offsetHeight > (videoHeight + 5)) {
|
|
||||||
playerSizePenalty = (element.offsetWidth - videoHeight) * sizePenaltyMultiplier;
|
|
||||||
}
|
|
||||||
if ( element.offsetWidth > (videoWidth + 5)) {
|
|
||||||
playerSizePenalty *= (element.offsetWidth - videoWidth) * sizePenaltyMultiplier
|
|
||||||
}
|
|
||||||
|
|
||||||
score -= playerSizePenalty;
|
|
||||||
|
|
||||||
elementQ.push({
|
|
||||||
element: element,
|
|
||||||
score: score,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
element = element.parentNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
// log player candidates
|
|
||||||
this.logger.log('info', 'playerDetect', 'player detect, auto/fallback: element queue and final element:', {queue: elementQ, bestCandidate: elementQ.length ? elementQ.sort( (a,b) => b.score - a.score)[0].element : 'n/a'});
|
|
||||||
|
|
||||||
if (elementQ.length) {
|
|
||||||
// return element with biggest score
|
|
||||||
const playerElement = elementQ.sort( (a,b) => b.score - a.score)[0].element;
|
|
||||||
|
|
||||||
this.updatePlayerDimensions(playerElement);
|
|
||||||
return playerElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if no candidates were found, something is obviously very, _very_ wrong.
|
|
||||||
// we return nothing. Player will be marked as invalid and setup will stop.
|
|
||||||
// VideoData should check for that before starting anything.
|
|
||||||
this.logger.log('warn', 'debug', '[PlayerData::getPlayer] no matching player was found for video', this.video, 'Extension cannot work on this site.');
|
|
||||||
return;
|
|
||||||
} catch (e) {
|
|
||||||
this.logger.log('crit', 'debug', '[PlayerData::getPlayer] something went wrong while detecting player:', e, 'Shutting down extension for this page');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
equalish(a,b, tolerance) {
|
|
||||||
return a > b - tolerance && a < b + tolerance;
|
|
||||||
}
|
|
||||||
|
|
||||||
forceDetectPlayerElementChange() {
|
|
||||||
// Player dimension changes get calculated every time updatePlayerDimensions is called (which happens
|
|
||||||
// every time getPlayer() detects an element). If updatePlayerDimension detects dimensions were changed,
|
|
||||||
// it will always re-apply current crop, rendering this function little more than a fancy alias for
|
|
||||||
// getPlayer().
|
|
||||||
this.getPlayer();
|
|
||||||
}
|
|
||||||
|
|
||||||
forceRefreshPlayerElement() {
|
|
||||||
this.getPlayer();
|
|
||||||
}
|
|
||||||
|
|
||||||
checkPlayerSizeChange() {
|
|
||||||
// this 'if' is just here for debugging — real code starts later. It's safe to collapse and
|
|
||||||
// ignore the contents of this if (unless we need to change how logging works)
|
|
||||||
if (this.logger.canLog('debug')){
|
|
||||||
if (this.dimensions?.fullscreen){
|
|
||||||
if(! PlayerData.isFullScreen()){
|
|
||||||
this.logger.log('info', 'debug', "[PlayerDetect] player size changed. reason: exited fullscreen");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if(! this.element) {
|
|
||||||
this.logger.log('info', 'playerDetect', "[PlayerDetect] player element isn't defined");
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( this.element &&
|
|
||||||
( +this.dimensions?.width != +this.element?.offsetWidth ||
|
|
||||||
+this.dimensions?.height != +this.element?.offsetHeight )
|
|
||||||
) {
|
|
||||||
this.logger.log('info', 'debug', "[PlayerDetect] player size changed. reason: dimension change. Old dimensions?", this.dimensions?.width, this.dimensions?.height, "new dimensions:", this.element?.offsetWidth, this.element?.offsetHeight);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// if size doesn't match, update & return true
|
|
||||||
if (this.dimensions?.width != this.element.offsetWidth
|
|
||||||
|| this.dimensions?.height != this.element.offsetHeight ){
|
|
||||||
|
|
||||||
const isFullScreen = PlayerData.isFullScreen();
|
|
||||||
|
|
||||||
if (isFullScreen) {
|
|
||||||
this.dimensions = {
|
|
||||||
width: window.innerWidth,
|
|
||||||
height: window.innerHeight,
|
|
||||||
fullscreen: true
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.dimensions = {
|
|
||||||
width: this.element.offsetWidth,
|
|
||||||
height: this.element.offsetHeight,
|
|
||||||
fullscreen: isFullScreen
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
checkFullscreenChange() {
|
|
||||||
const isFs = PlayerData.isFullScreen();
|
|
||||||
|
|
||||||
if (this.dimensions) {
|
|
||||||
if (this.dimensions.fullscreen != isFs) {
|
|
||||||
this.dimensions = {
|
|
||||||
fullscreen: isFs,
|
|
||||||
width: isFs ? screen.width : this.video.offsetWidth,
|
|
||||||
height: isFs ? screen.height : this.video.offsetHeight
|
|
||||||
};
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.log('info', 'debug', "[PlayerData::checkFullscreenChange] this.dimensions is not defined. Assuming fs change happened and setting default values.")
|
|
||||||
|
|
||||||
this.dimensions = {
|
|
||||||
fullscreen: isFs,
|
|
||||||
width: isFs ? screen.width : this.video.offsetWidth,
|
|
||||||
height: isFs ? screen.height : this.video.offsetHeight
|
|
||||||
};
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
showNotification(notificationId) {
|
|
||||||
this.notificationService?.showNotification(notificationId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* NOTE: this method needs to be deleted once Edge gets its shit together.
|
|
||||||
*/
|
|
||||||
showEdgeNotification() {
|
|
||||||
// if (BrowserDetect.isEdgeUA && !this.settings.active.mutedNotifications?.browserSpecific?.edge?.brokenDrm?.[window.hostname]) {
|
|
||||||
// this.ui = new PlayerUi(this.element, this.settings);
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.env.CHANNEL !== 'stable'){
|
|
||||||
console.info("PlayerData loaded");
|
|
||||||
}
|
|
||||||
|
|
||||||
export default PlayerData;
|
|
||||||
@ -1,797 +0,0 @@
|
|||||||
import Debug from '../../conf/Debug';
|
|
||||||
import PlayerData from './PlayerData';
|
|
||||||
import Resizer from '../video-transform/Resizer';
|
|
||||||
import ArDetector from '../ar-detect/ArDetector';
|
|
||||||
import AspectRatioType from '../../../common/enums/AspectRatioType.enum';
|
|
||||||
import * as _ from 'lodash';
|
|
||||||
import BrowserDetect from '../../conf/BrowserDetect';
|
|
||||||
import Logger from '../Logger';
|
|
||||||
import Settings from '../Settings';
|
|
||||||
import PageInfo from './PageInfo';
|
|
||||||
import { sleep } from '../../../common/js/utils';
|
|
||||||
import { hasDrm } from '../ar-detect/DrmDetecor';
|
|
||||||
|
|
||||||
class VideoData {
|
|
||||||
//#region flags
|
|
||||||
arSetupComplete: boolean = false;
|
|
||||||
destroyed: boolean = false;
|
|
||||||
invalid: boolean = false;
|
|
||||||
videoStatusOk: boolean = false;
|
|
||||||
videoLoaded: boolean = false;
|
|
||||||
videoDimensionsLoaded: boolean = false;
|
|
||||||
paused: boolean = false;
|
|
||||||
//#endregion
|
|
||||||
|
|
||||||
//#region misc stuff
|
|
||||||
vdid: string;
|
|
||||||
video: any;
|
|
||||||
observer: ResizeObserver;
|
|
||||||
mutationObserver: MutationObserver;
|
|
||||||
mutationObserverConf: MutationObserverInit = {
|
|
||||||
attributes: true,
|
|
||||||
attributeFilter: ['class', 'style'],
|
|
||||||
attributeOldValue: true,
|
|
||||||
};
|
|
||||||
extensionMode: any;
|
|
||||||
userCssClassName: string;
|
|
||||||
validationId: number;
|
|
||||||
dimensions: any;
|
|
||||||
//#endregion
|
|
||||||
|
|
||||||
//#region helper objects
|
|
||||||
logger: Logger;
|
|
||||||
settings: Settings;
|
|
||||||
pageInfo: PageInfo;
|
|
||||||
player: PlayerData;
|
|
||||||
resizer: Resizer;
|
|
||||||
arDetector: ArDetector;
|
|
||||||
//#endregion
|
|
||||||
|
|
||||||
|
|
||||||
get aspectRatio() {
|
|
||||||
try {
|
|
||||||
return this.video.videoWidth / this.video.videoHeight;
|
|
||||||
} catch (e) {
|
|
||||||
console.error('cannot determine stream aspect ratio!', e);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(video, settings, pageInfo){
|
|
||||||
this.vdid = (Math.random()*100).toFixed();
|
|
||||||
this.logger = pageInfo.logger;
|
|
||||||
this.arSetupComplete = false;
|
|
||||||
this.video = video;
|
|
||||||
this.destroyed = false;
|
|
||||||
this.settings = settings;
|
|
||||||
this.pageInfo = pageInfo;
|
|
||||||
this.extensionMode = pageInfo.extensionMode;
|
|
||||||
this.videoStatusOk = false;
|
|
||||||
|
|
||||||
this.userCssClassName = `uw-fuck-you-and-do-what-i-tell-you_${this.vdid}`;
|
|
||||||
|
|
||||||
this.videoLoaded = false;
|
|
||||||
this.videoDimensionsLoaded = true;
|
|
||||||
|
|
||||||
this.validationId = null;
|
|
||||||
|
|
||||||
this.dimensions = {
|
|
||||||
width: this.video.offsetWidth,
|
|
||||||
height: this.video.offsetHeight,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.setupStageOne();
|
|
||||||
}
|
|
||||||
|
|
||||||
async onVideoLoaded() {
|
|
||||||
if (!this.videoLoaded) {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* video.readyState 101:
|
|
||||||
* 0 — no info. Can't play.
|
|
||||||
* 1 — we have metadata but nothing else
|
|
||||||
* 2 — we have data for current playback position, but not future <--- meaning current frame, meaning Aard can work here or higher
|
|
||||||
* 3 — we have a lil bit for the future
|
|
||||||
* 4 — we'll survive to the end
|
|
||||||
*/
|
|
||||||
if (!this.video?.videoWidth || !this.video?.videoHeight || this.video.readyState < 2) {
|
|
||||||
return; // onVideoLoaded is a lie in this case
|
|
||||||
}
|
|
||||||
this.logger.log('info', 'init', '%c[VideoData::onVideoLoaded] ——————————— Initiating phase two of videoData setup ———————————', 'color: #0f9');
|
|
||||||
|
|
||||||
this.videoLoaded = true;
|
|
||||||
this.videoDimensionsLoaded = true;
|
|
||||||
try {
|
|
||||||
await this.setupStageTwo();
|
|
||||||
this.logger.log('info', 'init', '%c[VideoData::onVideoLoaded] ——————————— videoData setup stage two complete ———————————', 'color: #0f9');
|
|
||||||
} catch (e) {
|
|
||||||
this.logger.log('error', 'init', '%c[VideoData::onVideoLoaded] ——————————— Setup stage two failed. ———————————\n', 'color: #f00', e);
|
|
||||||
}
|
|
||||||
} else if (!this.videoDimensionsLoaded) {
|
|
||||||
this.logger.log('info', 'debug', "%c[VideoData::restoreCrop] Recovering from illegal video dimensions. Resetting aspect ratio.", "background: #afd, color: #132");
|
|
||||||
|
|
||||||
this.restoreCrop();
|
|
||||||
this.videoDimensionsLoaded = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
videoUnloaded() {
|
|
||||||
this.videoLoaded = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async injectBaseCss() {
|
|
||||||
try {
|
|
||||||
if (!this.mutationObserver) {
|
|
||||||
this.setupMutationObserver();
|
|
||||||
}
|
|
||||||
await this.pageInfo.injectCss(`
|
|
||||||
.uw-ultrawidify-base-wide-screen {
|
|
||||||
margin: 0px 0px 0px 0px !important;
|
|
||||||
width: initial !important;
|
|
||||||
align-self: start !important;
|
|
||||||
justify-self: start !important;
|
|
||||||
max-height: initial !important;
|
|
||||||
max-width: initial !important;
|
|
||||||
}
|
|
||||||
`);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to inject base css!', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
unsetBaseClass() {
|
|
||||||
this.mutationObserver.disconnect();
|
|
||||||
this.mutationObserver = undefined;
|
|
||||||
this.video.classList.remove('uw-ultrawidify-base-wide-screen');
|
|
||||||
}
|
|
||||||
|
|
||||||
//#region <video> event handlers
|
|
||||||
onLoadedData() {
|
|
||||||
this.logger.log('info', 'init', '[VideoData::ctor->video.onloadeddata] Video fired event "loaded data!"');
|
|
||||||
this.onVideoLoaded();
|
|
||||||
}
|
|
||||||
onLoadedMetadata() {
|
|
||||||
this.logger.log('info', 'init', '[VideoData::ctor->video.onloadedmetadata] Video fired event "loaded metadata!"');
|
|
||||||
this.onVideoLoaded();
|
|
||||||
}
|
|
||||||
onTimeUpdate() {
|
|
||||||
this.onVideoLoaded();
|
|
||||||
}
|
|
||||||
//#endregion
|
|
||||||
|
|
||||||
//#region lifecycle-ish
|
|
||||||
/**
|
|
||||||
* Injects base CSS and sets up handlers for <video> tag events
|
|
||||||
*/
|
|
||||||
async setupStageOne() {
|
|
||||||
this.logger.log('info', 'init', '%c[VideoData::setupStageOne] ——————————— Starting setup stage one! ———————————', 'color: #0f9');
|
|
||||||
// ensure base css is loaded before doing anything
|
|
||||||
this.injectBaseCss();
|
|
||||||
|
|
||||||
// this is in case extension loads before the video
|
|
||||||
this.video.addEventListener('loadeddata', this.onLoadedData.bind(this));
|
|
||||||
this.video.addEventListener('loadedmetadata', this.onLoadedMetadata.bind(this));
|
|
||||||
|
|
||||||
// this one is in case extension loads after the video is loaded
|
|
||||||
this.video.addEventListener('timeupdate', this.onTimeUpdate.bind(this));
|
|
||||||
|
|
||||||
this.logger.log('info', 'init', '%c[VideoData::setupStageOne] ——————————— Setup stage one complete! ———————————', 'color: #0f9');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Launches the extension for a given video (after the video element is defined well enough
|
|
||||||
* for our standards)
|
|
||||||
*/
|
|
||||||
async setupStageTwo() {
|
|
||||||
// POZOR: VRSTNI RED JE POMEMBEN (arDetect mora bit zadnji)
|
|
||||||
// NOTE: ORDERING OF OBJ INITIALIZATIONS IS IMPORTANT (arDetect needs to go last)
|
|
||||||
this.player = new PlayerData(this);
|
|
||||||
if (this.player.invalid) {
|
|
||||||
this.invalid = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.resizer = new Resizer(this);
|
|
||||||
|
|
||||||
// INIT OBSERVERS
|
|
||||||
try {
|
|
||||||
if (BrowserDetect.firefox) {
|
|
||||||
this.observer = new ResizeObserver(
|
|
||||||
_.debounce(
|
|
||||||
this.onVideoDimensionsChanged,
|
|
||||||
250,
|
|
||||||
{
|
|
||||||
leading: true,
|
|
||||||
trailing: true
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// Chrome for some reason insists that this.onPlayerDimensionsChanged is not a function
|
|
||||||
// when it's not wrapped into an anonymous function
|
|
||||||
this.observer = new ResizeObserver(
|
|
||||||
_.debounce(
|
|
||||||
(m, o) => {
|
|
||||||
this.onVideoDimensionsChanged(m, o)
|
|
||||||
},
|
|
||||||
250,
|
|
||||||
{
|
|
||||||
leading: true,
|
|
||||||
trailing: true
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('[VideoData] Observer setup failed:', e);
|
|
||||||
}
|
|
||||||
this.observer.observe(this.video);
|
|
||||||
|
|
||||||
// INIT AARD
|
|
||||||
this.arDetector = new ArDetector(this); // this starts Ar detection. needs optional parameter that prevets ardetdctor from starting
|
|
||||||
// player dimensions need to be in:
|
|
||||||
// this.player.dimensions
|
|
||||||
|
|
||||||
// apply default align and stretch
|
|
||||||
this.logger.log('info', 'debug', "%c[VideoData::ctor] Initial resizer reset!", "background: #afd, color: #132");
|
|
||||||
this.resizer.reset();
|
|
||||||
|
|
||||||
this.logger.log('info', ['debug', 'init'], '[VideoData::ctor] Created videoData with vdid', this.vdid, '\nextension mode:', this.extensionMode)
|
|
||||||
|
|
||||||
this.pageInfo.initMouseActionHandler(this);
|
|
||||||
|
|
||||||
// NOTE — since base class for our <video> element depends on player aspect ratio,
|
|
||||||
// we handle it in PlayerData class.
|
|
||||||
this.video.classList.add('uw-ultrawidify-base-wide-screen');
|
|
||||||
this.video.classList.add(this.userCssClassName); // this also needs to be applied BEFORE we initialize resizer!
|
|
||||||
|
|
||||||
|
|
||||||
// start fallback video/player size detection
|
|
||||||
this.fallbackChangeDetection();
|
|
||||||
|
|
||||||
// force reload last aspect ratio (if default crop ratio exists), but only after the video is
|
|
||||||
if (this.pageInfo.defaultCrop) {
|
|
||||||
this.resizer.setAr(this.pageInfo.defaultCrop);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (!this.pageInfo.defaultCrop) {
|
|
||||||
if (!this.invalid) {
|
|
||||||
this.initArDetection();
|
|
||||||
} else {
|
|
||||||
this.logger.log('error', 'debug', '[VideoData::secondStageSetup] Video is invalid. Aard not started.', this.video);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.logger.log('info', 'debug', '[VideoData::secondStageSetup] Default crop is specified for this site. Not starting aard.');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
this.logger.log('error', 'init', `[VideoData::secondStageSetup] Error with aard initialization (or error with default aspect ratio application)`, e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setupMutationObserver() {
|
|
||||||
try {
|
|
||||||
if (BrowserDetect.firefox) {
|
|
||||||
this.mutationObserver = new MutationObserver(
|
|
||||||
_.debounce(
|
|
||||||
this.onVideoMutation,
|
|
||||||
250,
|
|
||||||
{
|
|
||||||
leading: true,
|
|
||||||
trailing: true
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
// Chrome for some reason insists that this.onPlayerDimensionsChanged is not a function
|
|
||||||
// when it's not wrapped into an anonymous function
|
|
||||||
this.mutationObserver = new MutationObserver(
|
|
||||||
_.debounce(
|
|
||||||
(m, o) => {
|
|
||||||
this.onVideoMutation(m, o)
|
|
||||||
},
|
|
||||||
250,
|
|
||||||
{
|
|
||||||
leading: true,
|
|
||||||
trailing: true
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('[VideoData] Observer setup failed:', e);
|
|
||||||
}
|
|
||||||
this.mutationObserver.observe(this.video, this.mutationObserverConf);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* cleans up handlers and stuff when the show is over
|
|
||||||
*/
|
|
||||||
destroy() {
|
|
||||||
this.logger.log('info', ['debug', 'init'], `[VideoData::destroy] <vdid:${this.vdid}> received destroy command`);
|
|
||||||
|
|
||||||
if (this.video) {
|
|
||||||
this.video.classList.remove(this.userCssClassName);
|
|
||||||
this.video.classList.remove('uw-ultrawidify-base-wide-screen');
|
|
||||||
|
|
||||||
this.video.removeEventListener('onloadeddata', this.onLoadedData);
|
|
||||||
this.video.removeEventListener('onloadedmetadata', this.onLoadedMetadata);
|
|
||||||
this.video.removeEventListener('ontimeupdate', this.onTimeUpdate);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.pause();
|
|
||||||
this.destroyed = true;
|
|
||||||
try {
|
|
||||||
this.arDetector.stop();
|
|
||||||
this.arDetector.destroy();
|
|
||||||
} catch (e) {}
|
|
||||||
this.arDetector = undefined;
|
|
||||||
try {
|
|
||||||
this.resizer.destroy();
|
|
||||||
} catch (e) {}
|
|
||||||
this.resizer = undefined;
|
|
||||||
try {
|
|
||||||
this.player.destroy();
|
|
||||||
} catch (e) {}
|
|
||||||
try {
|
|
||||||
this.observer.disconnect();
|
|
||||||
} catch (e) {}
|
|
||||||
this.player = undefined;
|
|
||||||
this.video = undefined;
|
|
||||||
}
|
|
||||||
//#endregion
|
|
||||||
|
|
||||||
//#region video status
|
|
||||||
isVideoPlaying() {
|
|
||||||
return this.video && !!(this.video.currentTime > 0 && !this.video.paused && !this.video.ended && this.video.readyState > 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
hasVideoStartedPlaying() {
|
|
||||||
return this.video && this.video.currentTime > 0;
|
|
||||||
}
|
|
||||||
//#endregion
|
|
||||||
|
|
||||||
restoreCrop() {
|
|
||||||
this.logger.log('info', 'debug', '[VideoData::restoreCrop] Attempting to reset aspect ratio.')
|
|
||||||
// if we have default crop set for this page, apply this.
|
|
||||||
// otherwise, reset crop
|
|
||||||
if (this.pageInfo.defaultCrop) {
|
|
||||||
this.resizer.setAr(this.pageInfo.defaultCrop);
|
|
||||||
} else {
|
|
||||||
this.resizer.reset();
|
|
||||||
|
|
||||||
try {
|
|
||||||
this.stopArDetection();
|
|
||||||
this.startArDetection();
|
|
||||||
} catch (e) {
|
|
||||||
this.logger.log('warn', 'debug', '[VideoData::restoreCrop] Autodetection not resumed. Reason:', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Starts fallback change detection (validates whether currently applied settings are correct)
|
|
||||||
*/
|
|
||||||
async fallbackChangeDetection() {
|
|
||||||
const validationId = Date.now();
|
|
||||||
this.validationId = validationId;
|
|
||||||
|
|
||||||
while (!this.destroyed && !this.invalid && this.validationId === validationId) {
|
|
||||||
await sleep(500);
|
|
||||||
this.doPeriodicFallbackChangeDetectionCheck();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
doPeriodicFallbackChangeDetectionCheck() {
|
|
||||||
this.validateVideoOffsets();
|
|
||||||
}
|
|
||||||
|
|
||||||
onVideoMutation(mutationList?: MutationRecord[], observer?) {
|
|
||||||
// verify that mutation didn't remove our class. Some pages like to do that.
|
|
||||||
let confirmAspectRatioRestore = false;
|
|
||||||
|
|
||||||
if (!this.video) {
|
|
||||||
this.logger.log('error', 'debug', '[VideoData::onVideoMutation] mutation was triggered, but video element is missing. Something is fishy. Terminating this uw instance.');
|
|
||||||
this.destroy();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for(const mutation of mutationList) {
|
|
||||||
if (mutation.type === 'attributes') {
|
|
||||||
if( mutation.attributeName === 'class'
|
|
||||||
&& mutation.oldValue.indexOf('uw-ultrawidify-base-wide-screen') !== -1
|
|
||||||
&& !this.video.classList.contains('uw-ultrawidify-base-wide-screen')
|
|
||||||
) {
|
|
||||||
// force the page to include our class in classlist, if the classlist has been removed
|
|
||||||
// while classList.add() doesn't duplicate classes (does nothing if class is already added),
|
|
||||||
// we still only need to make sure we're only adding our class to classlist if it has been
|
|
||||||
// removed. classList.add() will _still_ trigger mutation (even if classlist wouldn't change).
|
|
||||||
// This is a problem because INFINITE RECURSION TIME, and we _really_ don't want that.
|
|
||||||
|
|
||||||
confirmAspectRatioRestore = true;
|
|
||||||
this.video.classList.add(this.userCssClassName);
|
|
||||||
this.video.classList.add('uw-ultrawidify-base-wide-screen');
|
|
||||||
} else if (mutation.attributeName === 'style') {
|
|
||||||
confirmAspectRatioRestore = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.processDimensionsChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
onVideoDimensionsChanged(mutationList, observer) {
|
|
||||||
if (!mutationList || this.video === undefined) { // something's wrong
|
|
||||||
if (observer && this.video) {
|
|
||||||
observer.disconnect();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.processDimensionsChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Forces Ultrawidify to resotre aspect ratio. You should never call this method directly,
|
|
||||||
* instead you should be calling processDimensionChanged() wrapper function.
|
|
||||||
*/
|
|
||||||
private _processDimensionsChanged() {
|
|
||||||
// adding player observer taught us that if element size gets triggered by a class, then
|
|
||||||
// the 'style' attributes don't necessarily trigger. This means we also need to trigger
|
|
||||||
// restoreAr here, in case video size was changed this way
|
|
||||||
this.player.forceRefreshPlayerElement();
|
|
||||||
this.restoreAr();
|
|
||||||
|
|
||||||
// sometimes something fucky wucky happens and mutations aren't detected correctly, so we
|
|
||||||
// try to get around that
|
|
||||||
setTimeout( () => {
|
|
||||||
this.validateVideoOffsets();
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Restores aspect ratio and validates video offsets after the restore. Execution uses
|
|
||||||
* debounce to limit how often the function executes.
|
|
||||||
*/
|
|
||||||
private processDimensionsChanged() {
|
|
||||||
_.debounce(
|
|
||||||
this._processDimensionsChanged,
|
|
||||||
250,
|
|
||||||
{
|
|
||||||
leading: true,
|
|
||||||
trailing: true
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
validateVideoOffsets() {
|
|
||||||
// validate if current video still exists. If not, we destroy current object
|
|
||||||
try {
|
|
||||||
if (! document.body.contains(this.video)) {
|
|
||||||
this.destroy();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
}
|
|
||||||
// THIS BREAKS PANNING
|
|
||||||
const videoComputedStyle = window.getComputedStyle(this.video);
|
|
||||||
const playerComputedStyle = window.getComputedStyle(this.player.element);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const transformMatrix = videoComputedStyle.transform.split(')')[0].split(',');
|
|
||||||
const translateX = +transformMatrix[4];
|
|
||||||
const translateY = +transformMatrix[5];
|
|
||||||
const vh = +(videoComputedStyle.height.split('px')[0]);
|
|
||||||
const vw = +(videoComputedStyle.width.split('px')[0]);
|
|
||||||
const ph = +(playerComputedStyle.height.split('px')[0]);
|
|
||||||
const pw = +(playerComputedStyle.width.split('px')[0]);
|
|
||||||
|
|
||||||
// TODO: check & account for panning and alignment
|
|
||||||
if (transformMatrix[0] !== 'none'
|
|
||||||
&& this.isWithin(vh, (ph - (translateY * 2)), 2)
|
|
||||||
&& this.isWithin(vw, (pw - (translateX * 2)), 2)) {
|
|
||||||
} else {
|
|
||||||
this.player.forceDetectPlayerElementChange();
|
|
||||||
this.restoreAr();
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch(e) {
|
|
||||||
console.error('Validating video offsets failed:', e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
isWithin(a, b, diff) {
|
|
||||||
return a < b + diff && a > b - diff
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the contents of the style attribute of the video element
|
|
||||||
* in a form of an object.
|
|
||||||
*/
|
|
||||||
getVideoStyle(): any {
|
|
||||||
// This will _always_ give us an array. Empty string gives an array
|
|
||||||
// that contains one element. That element is an empty string.
|
|
||||||
const styleArray = (this.video.getAttribute('style') || '').split(';');
|
|
||||||
|
|
||||||
const styleObject = {};
|
|
||||||
|
|
||||||
for (const style of styleArray) {
|
|
||||||
// not a valid CSS, so we skip those
|
|
||||||
if (style.indexOf(':') === -1) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// let's play _very_ safe
|
|
||||||
let [property, value] = style.split('!important')[0].split(':');
|
|
||||||
value = value.trim();
|
|
||||||
styleObject[property] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
return styleObject;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Some sites try to accomodate ultrawide users by "cropping" videos
|
|
||||||
* by setting 'style' attribute of the video element to 'height: X%',
|
|
||||||
* where 'X' is something greater than 100.
|
|
||||||
*
|
|
||||||
* This function gets that percentage and converts it into a factor.
|
|
||||||
*/
|
|
||||||
getHeightCompensationFactor() {
|
|
||||||
const heightStyle = this.getVideoStyle()?.height;
|
|
||||||
|
|
||||||
if (!heightStyle || !heightStyle.endsWith('%')) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const heightCompensationFactor = heightStyle.split('%')[0] / 100;
|
|
||||||
if (isNaN(heightCompensationFactor)) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
return heightCompensationFactor;
|
|
||||||
}
|
|
||||||
firstTimeArdInit(){
|
|
||||||
if(this.destroyed || this.invalid) {
|
|
||||||
// throw {error: 'VIDEO_DATA_DESTROYED', data: {videoData: this}};
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if(! this.arSetupComplete){
|
|
||||||
this.arDetector = new ArDetector(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
initArDetection() {
|
|
||||||
if(this.destroyed || this.invalid) {
|
|
||||||
// throw {error: 'VIDEO_DATA_DESTROYED', data: {videoData: this}};
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this.arDetector){
|
|
||||||
this.arDetector.init();
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
this.arDetector = new ArDetector(this);
|
|
||||||
this.arDetector.init();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
startArDetection() {
|
|
||||||
this.logger.log('info', 'debug', "[VideoData::startArDetection] starting AR detection")
|
|
||||||
if(this.destroyed || this.invalid) {
|
|
||||||
// throw {error: 'VIDEO_DATA_DESTROYED', data: {videoData: this}};
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasDrm(this.video)) {
|
|
||||||
this.player.showNotification('AARD_DRM');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.arDetector) {
|
|
||||||
this.initArDetection();
|
|
||||||
}
|
|
||||||
this.arDetector.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
rebootArDetection() {
|
|
||||||
if(this.destroyed || this.invalid) {
|
|
||||||
// throw {error: 'VIDEO_DATA_DESTROYED', data: {videoData: this}};
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.arDetector.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
stopArDetection() {
|
|
||||||
if (this.arDetector) {
|
|
||||||
this.arDetector.stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pause(){
|
|
||||||
this.paused = true;
|
|
||||||
if(this.arDetector){
|
|
||||||
this.arDetector.stop();
|
|
||||||
}
|
|
||||||
if(this.player){
|
|
||||||
this.player.stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resume(){
|
|
||||||
if(this.destroyed || this.invalid) {
|
|
||||||
// throw {error: 'VIDEO_DATA_DESTROYED', data: {videoData: this}};
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.paused = false;
|
|
||||||
try {
|
|
||||||
// this.resizer.start();
|
|
||||||
if (this.player) {
|
|
||||||
this.player.start();
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
this.logger.log('error', 'debug', "[VideoData.js::resume] cannot resume for reasons. Will destroy videoData. Error here:", e);
|
|
||||||
this.destroy();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resumeAutoAr(){
|
|
||||||
if(this.arDetector){
|
|
||||||
this.startArDetection();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setManualTick(manualTick) {
|
|
||||||
if(this.arDetector){
|
|
||||||
this.arDetector.setManualTick(manualTick);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tick() {
|
|
||||||
if(this.arDetector){
|
|
||||||
this.arDetector.tick();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setLastAr(lastAr){
|
|
||||||
if (this.invalid) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.resizer.setLastAr(lastAr);
|
|
||||||
}
|
|
||||||
|
|
||||||
setAr(ar, lastAr?){
|
|
||||||
if (this.invalid) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ar.type === AspectRatioType.Fixed || ar.type === AspectRatioType.FitHeight || ar.type === AspectRatioType.FitHeight) {
|
|
||||||
this.player.forceRefreshPlayerElement();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.resizer.setAr(ar, lastAr);
|
|
||||||
}
|
|
||||||
|
|
||||||
resetAr() {
|
|
||||||
if (this.invalid) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.resizer.reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
resetLastAr() {
|
|
||||||
if (this.invalid) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.resizer.setLastAr('original');
|
|
||||||
}
|
|
||||||
|
|
||||||
panHandler(event, forcePan?: boolean) {
|
|
||||||
if (this.invalid) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if(this.destroyed) {
|
|
||||||
// throw {error: 'VIDEO_DATA_DESTROYED', data: {videoData: this}};
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if(!this.resizer) {
|
|
||||||
this.destroy();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.resizer.panHandler(event, forcePan);
|
|
||||||
}
|
|
||||||
|
|
||||||
setPanMode(mode) {
|
|
||||||
if (this.invalid) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.resizer.setPanMode(mode);
|
|
||||||
}
|
|
||||||
|
|
||||||
setVideoAlignment(videoAlignment) {
|
|
||||||
if (this.invalid) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.resizer.setVideoAlignment(videoAlignment);
|
|
||||||
}
|
|
||||||
|
|
||||||
restoreAr(){
|
|
||||||
if (this.invalid) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.resizer.restore();
|
|
||||||
}
|
|
||||||
|
|
||||||
setStretchMode(stretchMode, fixedStretchRatio){
|
|
||||||
if (this.invalid) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.resizer.setStretchMode(stretchMode, fixedStretchRatio);
|
|
||||||
}
|
|
||||||
|
|
||||||
setZoom(zoomLevel, no_announce){
|
|
||||||
if (this.invalid) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.resizer.setZoom(zoomLevel, no_announce);
|
|
||||||
}
|
|
||||||
|
|
||||||
zoomStep(step){
|
|
||||||
if (this.invalid) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.resizer.zoomStep(step);
|
|
||||||
}
|
|
||||||
|
|
||||||
announceZoom(scale){
|
|
||||||
if (this.invalid) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.pageInfo.announceZoom(scale);
|
|
||||||
}
|
|
||||||
|
|
||||||
markPlayer(name, color) {
|
|
||||||
if (this.invalid) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this.player) {
|
|
||||||
this.player.markPlayer(name, color)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
unmarkPlayer() {
|
|
||||||
this.player.unmarkPlayer();
|
|
||||||
}
|
|
||||||
|
|
||||||
isPlaying() {
|
|
||||||
return this.video && this.video.currentTime > 0 && !this.video.paused && !this.video.ended;
|
|
||||||
}
|
|
||||||
|
|
||||||
checkVideoSizeChange(){
|
|
||||||
const videoWidth = this.video.offsetWidth;
|
|
||||||
const videoHeight = this.video.offsetHeight;
|
|
||||||
// this 'if' is just here for debugging — real code starts later. It's safe to collapse and
|
|
||||||
// ignore the contents of this if (unless we need to change how logging works)
|
|
||||||
if (this.logger.canLog('debug')){
|
|
||||||
if(! this.video) {
|
|
||||||
this.logger.log('info', 'videoDetect', "[VideoDetect] player element isn't defined");
|
|
||||||
}
|
|
||||||
if ( this.video &&
|
|
||||||
( this.dimensions?.width != videoWidth ||
|
|
||||||
this.dimensions?.height != videoHeight )
|
|
||||||
) {
|
|
||||||
this.logger.log('info', 'debug', "[VideoDetect] player size changed. reason: dimension change. Old dimensions?", this.dimensions.width, this.dimensions.height, "new dimensions:", this.video.offsetWidth, this.video.offsetHeight);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// if size doesn't match, update & return true
|
|
||||||
if (this.dimensions?.width != videoWidth
|
|
||||||
|| this.dimensions?.height != videoHeight ){
|
|
||||||
this.dimensions = {
|
|
||||||
width: videoWidth,
|
|
||||||
height: videoHeight,
|
|
||||||
};
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default VideoData;
|
|
||||||
@ -1,756 +0,0 @@
|
|||||||
import Debug from '../../conf/Debug';
|
|
||||||
import Scaler, { CropStrategy, VideoDimensions } from './Scaler';
|
|
||||||
import Stretcher from './Stretcher';
|
|
||||||
import Zoom from './Zoom';
|
|
||||||
import PlayerData from '../video-data/PlayerData';
|
|
||||||
import ExtensionMode from '../../../common/enums/ExtensionMode.enum';
|
|
||||||
import StretchType from '../../../common/enums/StretchType.enum';
|
|
||||||
import VideoAlignmentType from '../../../common/enums/VideoAlignmentType.enum';
|
|
||||||
import AspectRatioType from '../../../common/enums/AspectRatioType.enum';
|
|
||||||
import CropModePersistance from '../../../common/enums/CropModePersistence.enum';
|
|
||||||
import { sleep } from '../Util';
|
|
||||||
import Logger from '../Logger';
|
|
||||||
import Settings from '../Settings';
|
|
||||||
import VideoData from '../video-data/VideoData';
|
|
||||||
|
|
||||||
if(Debug.debug) {
|
|
||||||
console.log("Loading: Resizer.js");
|
|
||||||
}
|
|
||||||
|
|
||||||
class Resizer {
|
|
||||||
//#region flags
|
|
||||||
canPan: boolean = false;
|
|
||||||
destroyed: boolean = false;
|
|
||||||
//#endregion
|
|
||||||
|
|
||||||
//#region helper objects
|
|
||||||
logger: Logger;
|
|
||||||
settings: Settings;
|
|
||||||
scaler: Scaler;
|
|
||||||
stretcher: Stretcher;
|
|
||||||
zoom: Zoom;
|
|
||||||
conf: VideoData;
|
|
||||||
//#endregion
|
|
||||||
|
|
||||||
//#region HTML elements
|
|
||||||
video: any;
|
|
||||||
//#endregion
|
|
||||||
|
|
||||||
//#region data
|
|
||||||
correctedVideoDimensions: any;
|
|
||||||
currentCss: any;
|
|
||||||
currentStyleString: string;
|
|
||||||
currentPlayerStyleString: any;
|
|
||||||
currentCssValidFor: any;
|
|
||||||
currentVideoSettings: any;
|
|
||||||
lastAr: {type: any, ratio?: number} = {type: AspectRatioType.Initial};
|
|
||||||
resizerId: any;
|
|
||||||
videoAlignment: any;
|
|
||||||
userCss: string;
|
|
||||||
userCssClassName: any;
|
|
||||||
pan: any = null;
|
|
||||||
//#endregion
|
|
||||||
|
|
||||||
constructor(videoData) {
|
|
||||||
this.resizerId = (Math.random()*100).toFixed(0);
|
|
||||||
this.conf = videoData;
|
|
||||||
this.logger = videoData.logger;
|
|
||||||
this.video = videoData.video;
|
|
||||||
this.settings = videoData.settings;
|
|
||||||
|
|
||||||
this.scaler = new Scaler(this.conf);
|
|
||||||
this.stretcher = new Stretcher(this.conf);
|
|
||||||
this.zoom = new Zoom(this.conf);
|
|
||||||
|
|
||||||
this.videoAlignment = this.settings.getDefaultVideoAlignment(window.location.hostname); // this is initial video alignment
|
|
||||||
this.destroyed = false;
|
|
||||||
|
|
||||||
|
|
||||||
if (this.settings.active.pan) {
|
|
||||||
this.canPan = this.settings.active.miscSettings.mousePan.enabled;
|
|
||||||
} else {
|
|
||||||
this.canPan = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.userCssClassName = videoData.userCssClassName;
|
|
||||||
}
|
|
||||||
|
|
||||||
injectCss(css) {
|
|
||||||
this.conf.pageInfo.injectCss(css);
|
|
||||||
}
|
|
||||||
|
|
||||||
ejectCss(css) {
|
|
||||||
this.conf.pageInfo.ejectCss(css);
|
|
||||||
}
|
|
||||||
|
|
||||||
replaceCss(oldCss, newCss) {
|
|
||||||
this.conf.pageInfo.replaceCss(oldCss, newCss);
|
|
||||||
}
|
|
||||||
|
|
||||||
prepareCss(css) {
|
|
||||||
return `.${this.userCssClassName} {${css}}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
destroy(){
|
|
||||||
this.logger.log('info', ['debug', 'init'], `[Resizer::destroy] <rid:${this.resizerId}> received destroy command.`);
|
|
||||||
this.destroyed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
calculateRatioForLegacyOptions(ar){
|
|
||||||
// also present as modeToAr in Scaler.js
|
|
||||||
if (ar.type !== AspectRatioType.FitWidth && ar.type !== AspectRatioType.FitHeight && ar.ratio) {
|
|
||||||
return ar;
|
|
||||||
}
|
|
||||||
// Skrbi za "stare" možnosti, kot na primer "na širino zaslona", "na višino zaslona" in "ponastavi".
|
|
||||||
// Približevanje opuščeno.
|
|
||||||
// handles "legacy" options, such as 'fit to widht', 'fit to height' and AspectRatioType.Reset. No zoom tho
|
|
||||||
let ratioOut;
|
|
||||||
|
|
||||||
if (!this.conf.video) {
|
|
||||||
this.logger.log('info', 'debug', "[Scaler.js::modeToAr] No video??",this.conf.video, "killing videoData");
|
|
||||||
this.conf.destroy();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (! this.conf.player.dimensions) {
|
|
||||||
ratioOut = screen.width / screen.height;
|
|
||||||
} else {
|
|
||||||
this.logger.log('info', 'debug', `[Resizer::calculateRatioForLegacyOptions] <rid:${this.resizerId}> Player dimensions:`, this.conf.player.dimensions.width ,'x', this.conf.player.dimensions.height,'aspect ratio:', this.conf.player.dimensions.width / this.conf.player.dimensions.height)
|
|
||||||
ratioOut = this.conf.player.dimensions.width / this.conf.player.dimensions.height;
|
|
||||||
}
|
|
||||||
|
|
||||||
// POMEMBNO: lastAr je potrebno nastaviti šele po tem, ko kličemo _res_setAr(). _res_setAr() predvideva,
|
|
||||||
// da želimo nastaviti statično (type: 'static') razmerje stranic — tudi, če funkcijo kličemo tu oz. v ArDetect.
|
|
||||||
//
|
|
||||||
// IMPORTANT NOTE: lastAr needs to be set after _res_setAr() is called, as _res_setAr() assumes we're
|
|
||||||
// setting a static aspect ratio (even if the function is called from here or ArDetect).
|
|
||||||
|
|
||||||
let fileAr = this.conf.video.videoWidth / this.conf.video.videoHeight;
|
|
||||||
|
|
||||||
if (ar.type === AspectRatioType.FitWidth){
|
|
||||||
ar.ratio = ratioOut > fileAr ? ratioOut : fileAr;
|
|
||||||
}
|
|
||||||
else if(ar.type === AspectRatioType.FitHeight){
|
|
||||||
ar.ratio = ratioOut < fileAr ? ratioOut : fileAr;
|
|
||||||
}
|
|
||||||
else if(ar.type === AspectRatioType.Reset){
|
|
||||||
this.logger.log('info', 'debug', "[Scaler.js::modeToAr] Using original aspect ratio -", fileAr);
|
|
||||||
ar.ratio = fileAr;
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return ar;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
updateAr(ar) {
|
|
||||||
if (!ar) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Some options require a bit more testing re: whether they make sense
|
|
||||||
// if they don't, we refuse to update aspect ratio until they do
|
|
||||||
if (ar.type === AspectRatioType.Automatic || ar.type === AspectRatioType.Fixed) {
|
|
||||||
if (!ar.ratio || isNaN(ar.ratio)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only update aspect ratio if there's a difference between the old and the new state
|
|
||||||
if (!this.lastAr || ar.type !== this.lastAr.type || ar.ratio !== this.lastAr.ratio) {
|
|
||||||
this.setAr(ar);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async setAr(ar: {type: any, ratio?: number}, lastAr?: {type: any, ratio?: number}) {
|
|
||||||
if (this.destroyed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.video.videoWidth || !this.video.videoHeight) {
|
|
||||||
this.logger.log('warning', 'debug', '[Resizer::setAr] <rid:'+this.resizerId+'> Video has no width or no height. This is not allowed. Aspect ratio will not be set, and videoData will be uninitialized.');
|
|
||||||
this.conf.videoUnloaded();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.log('info', 'debug', '[Resizer::setAr] <rid:'+this.resizerId+'> trying to set ar. New ar:', ar);
|
|
||||||
|
|
||||||
if (ar == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const siteSettings = this.settings.active.sites[window.location.hostname];
|
|
||||||
let stretchFactors: {xFactor: number, yFactor: number, arCorrectionFactor?: number, ratio?: number} | any;
|
|
||||||
|
|
||||||
// reset zoom, but only on aspect ratio switch. We also know that aspect ratio gets converted to
|
|
||||||
// AspectRatioType.Fixed when zooming, so let's keep that in mind
|
|
||||||
if (
|
|
||||||
(ar.type !== AspectRatioType.Fixed && ar.type !== AspectRatioType.Manual) // anything not these two _always_ changes AR
|
|
||||||
|| ar.type !== this.lastAr.type // this also means aspect ratio has changed
|
|
||||||
|| ar.ratio !== this.lastAr.ratio // this also means aspect ratio has changed
|
|
||||||
) {
|
|
||||||
this.zoom.reset();
|
|
||||||
this.resetPan();
|
|
||||||
}
|
|
||||||
|
|
||||||
// most everything that could go wrong went wrong by this stage, and returns can happen afterwards
|
|
||||||
// this means here's the optimal place to set or forget aspect ratio. Saving of current crop ratio
|
|
||||||
// is handled in pageInfo.updateCurrentCrop(), which also makes sure to persist aspect ratio if ar
|
|
||||||
// is set to persist between videos / through current session / until manual reset.
|
|
||||||
if (ar.type === AspectRatioType.Automatic ||
|
|
||||||
ar.type === AspectRatioType.Reset ||
|
|
||||||
ar.type === AspectRatioType.Initial ) {
|
|
||||||
// reset/undo default
|
|
||||||
this.conf.pageInfo.updateCurrentCrop(undefined);
|
|
||||||
} else {
|
|
||||||
this.conf.pageInfo.updateCurrentCrop(ar);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lastAr) {
|
|
||||||
this.lastAr = this.calculateRatioForLegacyOptions(lastAr);
|
|
||||||
ar = this.calculateRatioForLegacyOptions(ar);
|
|
||||||
} else {
|
|
||||||
// NOTE: "fitw" "fith" and "reset" should ignore ar.ratio bit, but
|
|
||||||
// I'm not sure whether they do. Check that.
|
|
||||||
ar = this.calculateRatioForLegacyOptions(ar);
|
|
||||||
if (! ar) {
|
|
||||||
this.logger.log('info', 'resizer', `[Resizer::setAr] <${this.resizerId}> Something wrong with ar or the player. Doing nothing.`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.lastAr = {type: ar.type, ratio: ar.ratio}
|
|
||||||
}
|
|
||||||
|
|
||||||
// if (this.extensionMode === ExtensionMode.Basic && !PlayerData.isFullScreen() && ar.type !== AspectRatioType.Reset) {
|
|
||||||
// // don't actually apply or calculate css when using basic mode if not in fullscreen
|
|
||||||
// // ... unless we're resetting the aspect ratio to original
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
if (! this.video) {
|
|
||||||
this.conf.destroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
// pause AR on:
|
|
||||||
// * ar.type NOT automatic
|
|
||||||
// * ar.type is auto, but stretch is set to basic basic stretch
|
|
||||||
//
|
|
||||||
// unpause when using other modes
|
|
||||||
if (ar.type !== AspectRatioType.Automatic || this.stretcher.mode === StretchType.Basic) {
|
|
||||||
this.conf?.arDetector?.pause();
|
|
||||||
} else {
|
|
||||||
if (this.lastAr.type === AspectRatioType.Automatic) {
|
|
||||||
this.conf?.arDetector?.unpause();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// do stretch thingy
|
|
||||||
if (this.stretcher.mode === StretchType.NoStretch
|
|
||||||
|| this.stretcher.mode === StretchType.Conditional
|
|
||||||
|| this.stretcher.mode === StretchType.FixedSource){
|
|
||||||
|
|
||||||
stretchFactors = this.scaler.calculateCrop(ar);
|
|
||||||
|
|
||||||
if(! stretchFactors || stretchFactors.error){
|
|
||||||
this.logger.log('error', 'debug', `[Resizer::setAr] <rid:${this.resizerId}> failed to set AR due to problem with calculating crop. Error:`, stretchFactors?.error);
|
|
||||||
if (stretchFactors?.error === 'no_video'){
|
|
||||||
this.conf.destroy();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// we could have issued calculate crop too early. Let's tell VideoData that there's something wrong
|
|
||||||
// and exit this function. When <video> will receive onloadeddata or ontimeupdate (receiving either
|
|
||||||
// of the two means video is loaded or playing, and that means video has proper dimensions), it will
|
|
||||||
// try to reset or re-apply aspect ratio when the video is finally ready.
|
|
||||||
if (stretchFactors?.error === 'illegal_video_dimensions') {
|
|
||||||
this.conf.videoDimensionsLoaded = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.stretcher.mode === StretchType.Conditional){
|
|
||||||
this.stretcher.applyConditionalStretch(stretchFactors, ar.ratio);
|
|
||||||
} else if (this.stretcher.mode === StretchType.FixedSource) {
|
|
||||||
this.stretcher.applyStretchFixedSource(stretchFactors);
|
|
||||||
}
|
|
||||||
this.logger.log('info', 'debug', "[Resizer::setAr] Processed stretch factors for ",
|
|
||||||
this.stretcher.mode === StretchType.NoStretch ? 'stretch-free crop.' :
|
|
||||||
this.stretcher.mode === StretchType.Conditional ? 'crop with conditional StretchType.' : 'crop with fixed stretch',
|
|
||||||
'Stretch factors are:', stretchFactors
|
|
||||||
);
|
|
||||||
|
|
||||||
} else if (this.stretcher.mode === StretchType.Hybrid) {
|
|
||||||
stretchFactors = this.stretcher.calculateStretch(ar.ratio);
|
|
||||||
this.logger.log('info', 'debug', '[Resizer::setAr] Processed stretch factors for hybrid stretch/crop. Stretch factors are:', stretchFactors);
|
|
||||||
} else if (this.stretcher.mode === StretchType.Fixed) {
|
|
||||||
stretchFactors = this.stretcher.calculateStretchFixed(ar.ratio)
|
|
||||||
} else if (this.stretcher.mode === StretchType.Basic) {
|
|
||||||
stretchFactors = this.stretcher.calculateBasicStretch();
|
|
||||||
this.logger.log('info', 'debug', '[Resizer::setAr] Processed stretch factors for basic StretchType. Stretch factors are:', stretchFactors);
|
|
||||||
} else {
|
|
||||||
stretchFactors = {xFactor: 1, yFactor: 1};
|
|
||||||
this.logger.log('error', 'debug', '[Resizer::setAr] Okay wtf happened? If you see this, something has gone wrong', stretchFactors,"\n------[ i n f o d u m p ]------\nstretcher:", this.stretcher);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.zoom.applyZoom(stretchFactors);
|
|
||||||
|
|
||||||
this.stretcher.chromeBugMitigation(stretchFactors);
|
|
||||||
|
|
||||||
let translate = this.computeOffsets(stretchFactors);
|
|
||||||
|
|
||||||
this.applyCss(stretchFactors, translate);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
toFixedAr() {
|
|
||||||
// converting to fixed AR means we also turn off autoAR
|
|
||||||
this.setAr({
|
|
||||||
ratio: this.lastAr.ratio,
|
|
||||||
type: AspectRatioType.Fixed
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
resetLastAr() {
|
|
||||||
this.lastAr = {type: AspectRatioType.Initial};
|
|
||||||
}
|
|
||||||
|
|
||||||
setLastAr(override){
|
|
||||||
this.lastAr = override;
|
|
||||||
}
|
|
||||||
|
|
||||||
getLastAr(){
|
|
||||||
return this.lastAr;
|
|
||||||
}
|
|
||||||
|
|
||||||
setStretchMode(stretchMode, fixedStretchRatio?){
|
|
||||||
this.stretcher.setStretchMode(stretchMode, fixedStretchRatio);
|
|
||||||
this.restore();
|
|
||||||
}
|
|
||||||
|
|
||||||
panHandler(event, forcePan) {
|
|
||||||
if (this.canPan || forcePan) {
|
|
||||||
if(!this.conf.player || !this.conf.player.element) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// dont allow weird floats
|
|
||||||
this.videoAlignment = VideoAlignmentType.Center;
|
|
||||||
|
|
||||||
// because non-fixed aspect ratios reset panning:
|
|
||||||
if (this.lastAr.type !== AspectRatioType.Fixed) {
|
|
||||||
this.toFixedAr();
|
|
||||||
}
|
|
||||||
|
|
||||||
const player = this.conf.player.element;
|
|
||||||
|
|
||||||
const relativeX = (event.pageX - player.offsetLeft) / player.offsetWidth;
|
|
||||||
const relativeY = (event.pageY - player.offsetTop) / player.offsetHeight;
|
|
||||||
|
|
||||||
this.logger.log('info', 'mousemove', "[Resizer::panHandler] mousemove.pageX, pageY:", event.pageX, event.pageY, "\nrelativeX/Y:", relativeX, relativeY)
|
|
||||||
|
|
||||||
this.setPan(relativeX, relativeY);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resetPan() {
|
|
||||||
this.pan = {x: 0, y: 0};
|
|
||||||
this.videoAlignment = this.settings.getDefaultVideoAlignment(window.location.hostname);
|
|
||||||
}
|
|
||||||
|
|
||||||
setPan(relativeMousePosX, relativeMousePosY){
|
|
||||||
// relativeMousePos[X|Y] - on scale from 0 to 1, how close is the mouse to player edges.
|
|
||||||
// use these values: top, left: 0, bottom, right: 1
|
|
||||||
if(! this.pan){
|
|
||||||
this.pan = {x: 0, y: 0};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.settings.active.miscSettings.mousePanReverseMouse) {
|
|
||||||
this.pan.relativeOffsetX = (relativeMousePosX * 1.1) - 0.55;
|
|
||||||
this.pan.relativeOffsetY = (relativeMousePosY * 1.1) - 0.55;
|
|
||||||
} else {
|
|
||||||
this.pan.relativeOffsetX = -(relativeMousePosX * 1.1) + 0.55;
|
|
||||||
this.pan.relativeOffsetY = -(relativeMousePosY * 1.1) + 0.55;
|
|
||||||
}
|
|
||||||
this.restore();
|
|
||||||
}
|
|
||||||
|
|
||||||
setVideoAlignment(videoAlignment) {
|
|
||||||
this.videoAlignment = videoAlignment;
|
|
||||||
this.restore();
|
|
||||||
}
|
|
||||||
|
|
||||||
restore() {
|
|
||||||
this.logger.log('info', 'debug', "[Resizer::restore] <rid:"+this.resizerId+"> attempting to restore aspect ratio", {'lastAr': this.lastAr} );
|
|
||||||
|
|
||||||
// this is true until we verify that css has actually been applied
|
|
||||||
if(this.lastAr.type === AspectRatioType.Initial){
|
|
||||||
this.setAr({type: AspectRatioType.Reset});
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
if (this.lastAr?.ratio === null) {
|
|
||||||
// if this is the case, we do nothing as we have the correct aspect ratio
|
|
||||||
// throw "Last ar is null!"
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.setAr(this.lastAr, this.lastAr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
reset(){
|
|
||||||
this.setStretchMode(this.settings.active.sites[window.location.hostname]?.stretch ?? this.settings.active.sites['@global'].stretch);
|
|
||||||
this.zoom.setZoom(1);
|
|
||||||
this.resetPan();
|
|
||||||
this.setAr({type: AspectRatioType.Reset});
|
|
||||||
}
|
|
||||||
|
|
||||||
setPanMode(mode) {
|
|
||||||
if (mode === 'enable') {
|
|
||||||
this.canPan = true;
|
|
||||||
} else if (mode === 'disable') {
|
|
||||||
this.canPan = false;
|
|
||||||
} else if (mode === 'toggle') {
|
|
||||||
this.canPan = !this.canPan;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setZoom(zoomLevel, no_announce) {
|
|
||||||
this.zoom.setZoom(zoomLevel, no_announce);
|
|
||||||
}
|
|
||||||
|
|
||||||
zoomStep(step){
|
|
||||||
this.zoom.zoomStep(step);
|
|
||||||
}
|
|
||||||
|
|
||||||
resetZoom(){
|
|
||||||
this.zoom.setZoom(1);
|
|
||||||
this.restore();
|
|
||||||
}
|
|
||||||
|
|
||||||
resetCrop(){
|
|
||||||
this.setAr({type: AspectRatioType.Reset});
|
|
||||||
}
|
|
||||||
|
|
||||||
resetStretch(){
|
|
||||||
this.stretcher.setStretchMode(StretchType.NoStretch);
|
|
||||||
this.restore();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// mostly internal stuff
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the size of the video file _as displayed_ on the screen.
|
|
||||||
* Consider the following example:
|
|
||||||
*
|
|
||||||
* * player dimensions are 2560x1080
|
|
||||||
* * <video> is child of player
|
|
||||||
* * <video> has the following css: {width: 100%, height: 100%}
|
|
||||||
* * video file dimensions are 1280x720
|
|
||||||
*
|
|
||||||
* CSS will ensure that the dimensions of <video> tag are equal to the dimension of the
|
|
||||||
* player element — that is, 2560x1080px. This is no bueno, because the browser will upscale
|
|
||||||
* the video file to take up as much space as it can (without stretching it). This means
|
|
||||||
* we'll get a 1920x1080 video (as displayed) and a letterbox.
|
|
||||||
*
|
|
||||||
* We can't get that number out of anywhere: video.videoWidth will return 1280 (video file
|
|
||||||
* dimensions) and .offsetWidth (and the likes) will return the <video> tag dimension. Neither
|
|
||||||
* will return the actual size of video as displayed, which we need in order to calculate the
|
|
||||||
* extra space to the left and right of the video.
|
|
||||||
*
|
|
||||||
* We make the assumption of the
|
|
||||||
*/
|
|
||||||
computeVideoDisplayedDimensions() {
|
|
||||||
const offsetWidth = this.conf.video.offsetWidth;
|
|
||||||
const offsetHeight = this.conf.video.offsetHeight;
|
|
||||||
|
|
||||||
const scaleX = offsetWidth / this.conf.video.videoWidth;
|
|
||||||
const scaleY = offsetHeight / this.conf.video.videoHeight;
|
|
||||||
|
|
||||||
// if differences between the scale factors are minimal, we presume offsetWidth and
|
|
||||||
// offsetHeight are the accurate enough for our needs
|
|
||||||
if (Math.abs(scaleX - scaleY) < 0.02) {
|
|
||||||
return {
|
|
||||||
realVideoWidth: offsetWidth,
|
|
||||||
realVideoHeight: offsetHeight,
|
|
||||||
marginX: 0,
|
|
||||||
marginY: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// if we're still here, we need to calculate real video dimensions
|
|
||||||
const diffX = Math.abs(scaleY * this.conf.video.videoWidth - offsetWidth);
|
|
||||||
const diffY = Math.abs(scaleX * this.conf.video.videoHeight - offsetHeight);
|
|
||||||
|
|
||||||
// in this case, we want to base our real dimensions off scaleX
|
|
||||||
// otherwise, we want to base it off scaleY
|
|
||||||
if (diffX < diffY) {
|
|
||||||
const realHeight = this.conf.video.videoHeight * scaleX;
|
|
||||||
return {
|
|
||||||
realVideoWidth: offsetWidth,
|
|
||||||
realVideoHeight: realHeight,
|
|
||||||
marginX: 0,
|
|
||||||
marginY: (offsetHeight - realHeight) * 0.5
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const realWidth = this.conf.video.videoWidth * scaleY;
|
|
||||||
return {
|
|
||||||
realVideoWidth: realWidth,
|
|
||||||
realVideoHeight: offsetHeight,
|
|
||||||
marginX: (offsetWidth - realWidth) * 0.5,
|
|
||||||
marginY: 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sometimes, sites (e.g. new reddit) will guarantee that video fits width of its container
|
|
||||||
* and let the browser figure out the height through the magic of height:auto. This is bad,
|
|
||||||
* because our addon generally relies of videos always being 100% of the height of the
|
|
||||||
* container.
|
|
||||||
*
|
|
||||||
* This sometimes leads to a situation where realVideoHeight and realVideoWidth — at least
|
|
||||||
* one of which should be roughly equal to the player width or hight with the other one being
|
|
||||||
* either smaller or equal — are both smaller than player width or height; and sometimes
|
|
||||||
* rather substantially. Fortunately for us, realVideo[Width|Height] and player dimensions
|
|
||||||
* never lie, which allows us to calculate the extra scale factor we need.
|
|
||||||
*
|
|
||||||
* Returned factor for this function should do fit:contain, not fit:cover.
|
|
||||||
* @param realVideoWidth real video width
|
|
||||||
* @param realVideoHeight real video height
|
|
||||||
* @param playerWidth player width
|
|
||||||
* @param playerHeight player height
|
|
||||||
* @param mode whether to
|
|
||||||
*/
|
|
||||||
computeAutoHeightCompensationFactor(realVideoWidth: number, realVideoHeight: number, playerWidth: number, playerHeight: number, mode: 'height' | 'width'): number {
|
|
||||||
const widthFactor = playerWidth / realVideoWidth;
|
|
||||||
const heightFactor = playerHeight / realVideoHeight;
|
|
||||||
|
|
||||||
return mode === 'height' ? heightFactor : widthFactor;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _computeOffsetsRecursionGuard: boolean = false;
|
|
||||||
computeOffsets(stretchFactors: VideoDimensions){
|
|
||||||
this.logger.log('info', 'debug', "[Resizer::computeOffsets] <rid:"+this.resizerId+"> video will be aligned to ", this.settings.active.sites['@global'].videoAlignment);
|
|
||||||
|
|
||||||
const {realVideoWidth, realVideoHeight, marginX, marginY} = this.computeVideoDisplayedDimensions();
|
|
||||||
|
|
||||||
// correct any remaining element size discrepencies (applicable only to certain crop strategies!)
|
|
||||||
// NOTE: it's possible that we might also need to apply a similar measure for CropPillarbox strategy
|
|
||||||
// (but we'll wait for bug reports before doing so).
|
|
||||||
// We also don't compensate for height:auto if height is provided via element style
|
|
||||||
let autoHeightCompensationFactor;
|
|
||||||
if (
|
|
||||||
stretchFactors.cropStrategy === CropStrategy.CropLetterbox
|
|
||||||
&& (!stretchFactors.styleHeightCompensationFactor || stretchFactors.styleHeightCompensationFactor === 1)
|
|
||||||
) {
|
|
||||||
autoHeightCompensationFactor = this.computeAutoHeightCompensationFactor(realVideoWidth, realVideoHeight, this.conf.player.dimensions.width, this.conf.player.dimensions.height, 'height');
|
|
||||||
stretchFactors.xFactor *= autoHeightCompensationFactor;
|
|
||||||
stretchFactors.yFactor *= autoHeightCompensationFactor;
|
|
||||||
}
|
|
||||||
|
|
||||||
const wdiff = this.conf.player.dimensions.width - realVideoWidth;
|
|
||||||
const hdiff = this.conf.player.dimensions.height - realVideoHeight;
|
|
||||||
|
|
||||||
const wdiffAfterZoom = realVideoWidth * stretchFactors.xFactor - this.conf.player.dimensions.width;
|
|
||||||
const hdiffAfterZoom = realVideoHeight * stretchFactors.yFactor - this.conf.player.dimensions.height;
|
|
||||||
|
|
||||||
const translate = {
|
|
||||||
x: wdiff * 0.5,
|
|
||||||
y: hdiff * 0.5,
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (this.pan.relativeOffsetX || this.pan.relativeOffsetY) {
|
|
||||||
// don't offset when video is smaller than player
|
|
||||||
if(wdiffAfterZoom >= 0 || hdiffAfterZoom >= 0) {
|
|
||||||
translate.x += wdiffAfterZoom * this.pan.relativeOffsetX * this.zoom.scale;
|
|
||||||
translate.y += hdiffAfterZoom * this.pan.relativeOffsetY * this.zoom.scale;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (this.videoAlignment == VideoAlignmentType.Left) {
|
|
||||||
translate.x += wdiffAfterZoom * 0.5;
|
|
||||||
}
|
|
||||||
else if (this.videoAlignment == VideoAlignmentType.Right) {
|
|
||||||
translate.x -= wdiffAfterZoom * 0.5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.log(
|
|
||||||
'info', ['debug', 'resizer'], "[Resizer::_res_computeOffsets] <rid:"+this.resizerId+"> calculated offsets:",
|
|
||||||
'\n\n---- elements ----',
|
|
||||||
'\nplayer element: ', this.conf.player.element,
|
|
||||||
'\nvideo element: ', this.conf.video,
|
|
||||||
'\n\n---- data in ----',
|
|
||||||
'\nplayer dimensions: ', {w: this.conf.player.dimensions.width, h: this.conf.player.dimensions.height},
|
|
||||||
'\nvideo dimensions: ', {w: this.conf.video.offsetWidth, h: this.conf.video.offsetHeight},
|
|
||||||
'\nreal video dimensions:', {w: realVideoWidth, h: realVideoHeight},
|
|
||||||
'\nauto compensation: ', 'x', autoHeightCompensationFactor,
|
|
||||||
'\nstretch factors: ', stretchFactors,
|
|
||||||
'\npan & zoom: ', this.pan, this.zoom.scale,
|
|
||||||
'\nwdiff, hdiff: ', wdiff, 'x', hdiff,
|
|
||||||
'\nwdiff, hdiffAfterZoom:', wdiffAfterZoom, 'x', hdiffAfterZoom,
|
|
||||||
'\n\n---- data out ----\n',
|
|
||||||
'translate:', translate
|
|
||||||
);
|
|
||||||
|
|
||||||
// by the way, let's do a quick sanity check whether video player is doing any fuckies wuckies
|
|
||||||
// fucky wucky examples:
|
|
||||||
//
|
|
||||||
// * video width is bigger than player width AND video height is bigger than player height
|
|
||||||
// * video width is smaller than player width AND video height is smaller than player height
|
|
||||||
//
|
|
||||||
// In both examples, at most one of the two conditions can be true at the same time. If both
|
|
||||||
// conditions are true at the same time, we need to go 'chiny reckon' and recheck our player
|
|
||||||
// element. Chances are our video is not getting aligned correctly
|
|
||||||
if (
|
|
||||||
(this.conf.video.offsetWidth > this.conf.player.dimensions.width && this.conf.video.offsetHeight > this.conf.player.dimensions.height) ||
|
|
||||||
(this.conf.video.offsetWidth < this.conf.player.dimensions.width && this.conf.video.offsetHeight < this.conf.player.dimensions.height)
|
|
||||||
) {
|
|
||||||
this.logger.log('warn', ['debugger', 'resizer'], `[Resizer::_res_computeOffsets] <rid:${this.resizerId}> We are getting some incredibly funny results here.\n\n`,
|
|
||||||
`Video seems to be both wider and taller (or shorter and narrower) than player element at the same time. This is super duper not supposed to happen.\n\n`,
|
|
||||||
`Player element needs to be checked.`
|
|
||||||
)
|
|
||||||
|
|
||||||
// sometimes this appears to randomly recurse.
|
|
||||||
// There seems to be no way to reproduce it.
|
|
||||||
if (! this._computeOffsetsRecursionGuard) {
|
|
||||||
this._computeOffsetsRecursionGuard = true;
|
|
||||||
if (this.conf.player.checkPlayerSizeChange()) {
|
|
||||||
this.conf.player.onPlayerDimensionsChanged();
|
|
||||||
}
|
|
||||||
this._computeOffsetsRecursionGuard = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return translate;
|
|
||||||
}
|
|
||||||
|
|
||||||
//#region css handling
|
|
||||||
buildStyleArray(existingStyleString, extraStyleString) {
|
|
||||||
if (existingStyleString) {
|
|
||||||
const styleArray = existingStyleString.split(";");
|
|
||||||
|
|
||||||
if (extraStyleString) {
|
|
||||||
const extraCss = extraStyleString.split(';');
|
|
||||||
let dup = false;
|
|
||||||
|
|
||||||
for (const ecss of extraCss) {
|
|
||||||
for (let i in styleArray) {
|
|
||||||
if (ecss.split(':')[0].trim() === styleArray[i].split(':')[0].trim()) {
|
|
||||||
dup = true;
|
|
||||||
styleArray[i] = ecss;
|
|
||||||
}
|
|
||||||
if (dup) {
|
|
||||||
dup = false;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
styleArray.push(ecss);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i in styleArray) {
|
|
||||||
styleArray[i] = styleArray[i].trim();
|
|
||||||
// some sites do 'top: 50%; left: 50%; transform: <transform>' to center videos.
|
|
||||||
// we dont wanna, because we already center videos on our own
|
|
||||||
if (styleArray[i].startsWith("transform:")
|
|
||||||
|| styleArray[i].startsWith("top:")
|
|
||||||
|| styleArray[i].startsWith("left:")
|
|
||||||
|| styleArray[i].startsWith("right:")
|
|
||||||
|| styleArray[i].startsWith("bottom:")
|
|
||||||
|| styleArray[i].startsWith("margin")
|
|
||||||
){
|
|
||||||
delete styleArray[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return styleArray;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
buildStyleString(styleArray) {
|
|
||||||
let styleString = '';
|
|
||||||
|
|
||||||
for(let i in styleArray) {
|
|
||||||
if(styleArray[i]) {
|
|
||||||
styleString += styleArray[i] + " !important; ";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return styleString;
|
|
||||||
}
|
|
||||||
|
|
||||||
applyCss(stretchFactors, translate){
|
|
||||||
// apply extra CSS here. In case of duplicated properties, extraCss overrides
|
|
||||||
// default styleString
|
|
||||||
if (! this.video) {
|
|
||||||
this.logger.log('warn', 'debug', "[Resizer::applyCss] <rid:"+this.resizerId+"> Video went missing, doing nothing.");
|
|
||||||
|
|
||||||
this.conf.destroy();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.log('info', ['debug', 'resizer'], "[Resizer::applyCss] <rid:"+this.resizerId+"> will apply css.", {stretchFactors, translate});
|
|
||||||
|
|
||||||
// save stuff for quick tests (before we turn numbers into css values):
|
|
||||||
this.currentVideoSettings = {
|
|
||||||
validFor: this.conf.player.dimensions,
|
|
||||||
// videoWidth: dimensions.width,
|
|
||||||
// videoHeight: dimensions.height
|
|
||||||
}
|
|
||||||
|
|
||||||
let extraStyleString;
|
|
||||||
try {
|
|
||||||
extraStyleString = this.settings.active.sites[window.location.hostname].DOM.video.additionalCss;
|
|
||||||
} catch (e) {
|
|
||||||
// do nothing. It's ok if no special settings are defined for this site, we'll just do defaults
|
|
||||||
}
|
|
||||||
|
|
||||||
const styleArray = this.buildStyleArray('', extraStyleString)
|
|
||||||
|
|
||||||
// add remaining elements
|
|
||||||
if (stretchFactors) {
|
|
||||||
styleArray.push(`transform: translate(${translate.x}px, ${translate.y}px) scale(${stretchFactors.xFactor}, ${stretchFactors.yFactor}) !important;`);
|
|
||||||
|
|
||||||
// important — guarantees video will be properly aligned
|
|
||||||
// Note that position:absolute cannot be put here, otherwise old.reddit /w RES breaks — videos embedded
|
|
||||||
// from certain hosts will get a height: 0px. This is bad.
|
|
||||||
styleArray.push("top: 0px !important; left: 0px !important; bottom: 0px !important; right: 0px;");
|
|
||||||
|
|
||||||
// important — some websites (cough reddit redesign cough) may impose some dumb max-width and max-height
|
|
||||||
// restrictions. If site has dumb shit like 'max-width: 100%' and 'max-height: 100vh' in their CSS, that
|
|
||||||
// shit will prevent us from applying desired crop. This means we need to tell websites to fuck off with
|
|
||||||
// that crap. We know better.
|
|
||||||
styleArray.push("max-width: none !important; max-height: none !important;");
|
|
||||||
}
|
|
||||||
const styleString = `${this.buildStyleString(styleArray)}${extraStyleString || ''}`; // string returned by buildStyleString() should end with ; anyway
|
|
||||||
|
|
||||||
// build style string back
|
|
||||||
this.setStyleString(styleString);
|
|
||||||
}
|
|
||||||
|
|
||||||
setStyleString (styleString) {
|
|
||||||
this.currentCssValidFor = this.conf.player.dimensions;
|
|
||||||
const newCssString = this.prepareCss(styleString);
|
|
||||||
|
|
||||||
// inject new CSS or replace existing one
|
|
||||||
if (!this.userCss) {
|
|
||||||
this.logger.log('info', ['debug', 'resizer'], "[Resizer::setStyleString] <rid:"+this.resizerId+"> Setting new css: ", newCssString);
|
|
||||||
|
|
||||||
this.injectCss(newCssString);
|
|
||||||
this.userCss = newCssString;
|
|
||||||
} else if (newCssString !== this.userCss) {
|
|
||||||
this.logger.log('info', ['debug', 'resizer'], "[Resizer::setStyleString] <rid:"+this.resizerId+"> Replacing css.\nOld string:", this.userCss, "\nNew string:", newCssString);
|
|
||||||
// we only replace css if it
|
|
||||||
this.replaceCss(this.userCss, newCssString);
|
|
||||||
this.userCss = newCssString;
|
|
||||||
} else {
|
|
||||||
this.logger.log('info', ['debug', 'resizer'], "[Resizer::setStyleString] <rid:"+this.resizerId+"> Existing css is still valid, doing nothing.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//#endregion
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Resizer;
|
|
||||||
@ -1,88 +0,0 @@
|
|||||||
import Debug from '../../conf/Debug';
|
|
||||||
import Logger from '../Logger';
|
|
||||||
import VideoData from '../video-data/VideoData';
|
|
||||||
|
|
||||||
// računa približevanje ter računa/popravlja odmike videa
|
|
||||||
// calculates zooming and video offsets/panning
|
|
||||||
|
|
||||||
class Zoom {
|
|
||||||
//#region flags
|
|
||||||
|
|
||||||
//#endregion
|
|
||||||
|
|
||||||
//#region helper objects
|
|
||||||
conf: VideoData;
|
|
||||||
logger: Logger;
|
|
||||||
//#endregion
|
|
||||||
|
|
||||||
//#region misc data
|
|
||||||
scale: number = 1;
|
|
||||||
logScale: number = 0;
|
|
||||||
scaleStep: number = 0.1;
|
|
||||||
minScale: number = -1; // 50% (log2(0.5) = -1)
|
|
||||||
maxScale: number = 3; // 800% (log2(8) = 3)
|
|
||||||
//#endregion
|
|
||||||
|
|
||||||
|
|
||||||
constructor(videoData) {
|
|
||||||
this.conf = videoData;
|
|
||||||
this.logger = videoData.logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
reset(){
|
|
||||||
this.scale = 1;
|
|
||||||
this.logScale = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
zoomStep(amount){
|
|
||||||
this.logScale += amount;
|
|
||||||
|
|
||||||
if (this.logScale <= this.minScale) {
|
|
||||||
this.logScale = this.minScale;
|
|
||||||
}
|
|
||||||
if (this.logScale >= this.maxScale) {
|
|
||||||
this.logScale = this.maxScale;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.scale = Math.pow(2, this.logScale);
|
|
||||||
|
|
||||||
this.logger.log('info', 'debug', "[Zoom::zoomStep] changing zoom by", amount, ". New zoom level:", this.scale);
|
|
||||||
|
|
||||||
this.conf.resizer.toFixedAr();
|
|
||||||
|
|
||||||
this.conf.restoreAr();
|
|
||||||
this.conf.announceZoom(this.scale);
|
|
||||||
}
|
|
||||||
|
|
||||||
setZoom(scale: number, no_announce?){
|
|
||||||
this.logger.log('info', 'debug', "[Zoom::setZoom] Setting zoom to", scale, "!");
|
|
||||||
|
|
||||||
// NOTE: SCALE IS NOT LOGARITHMIC
|
|
||||||
if(scale < Math.pow(2, this.minScale)) {
|
|
||||||
scale = this.minScale;
|
|
||||||
} else if (scale > Math.pow(2, this.maxScale)) {
|
|
||||||
scale = this.maxScale;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.scale = scale;
|
|
||||||
|
|
||||||
this.conf.restoreAr();
|
|
||||||
if (!no_announce) {
|
|
||||||
this.conf.announceZoom(this.scale);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
applyZoom(stretchFactors){
|
|
||||||
if (!stretchFactors) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.logger.log('info', 'debug', "[Zoom::setZoom] Applying zoom. Stretch factors pre:", stretchFactors, " —> scale:", this.scale);
|
|
||||||
|
|
||||||
stretchFactors.xFactor *= this.scale;
|
|
||||||
stretchFactors.yFactor *= this.scale;
|
|
||||||
|
|
||||||
this.logger.log('info', 'debug', "[Zoom::setZoom] Applying zoom. Stretch factors post:", stretchFactors);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Zoom;
|
|
||||||
229
src/ext/module/EventBus.ts
Normal file
229
src/ext/module/EventBus.ts
Normal file
@ -0,0 +1,229 @@
|
|||||||
|
import { EventBusCommand, EventBusContext, EventBusMessage } from '@src/common/interfaces/EventBusMessage.interface';
|
||||||
|
import { IframeTunnelPayload } from '@src/common/interfaces/IframeTunnelPayload.interface';
|
||||||
|
import Comms from '@src/ext/module/comms/Comms';
|
||||||
|
import CommsClient, { CommsOrigin } from '@src/ext/module/comms/CommsClient';
|
||||||
|
import CommsServer from '@src/ext/module/comms/CommsServer';
|
||||||
|
|
||||||
|
|
||||||
|
export default class EventBus {
|
||||||
|
|
||||||
|
private name: string;
|
||||||
|
private uuid = crypto.randomUUID();
|
||||||
|
|
||||||
|
private commands: { [x: string]: EventBusCommand[]} = {};
|
||||||
|
private comms?: CommsClient | CommsServer;
|
||||||
|
private commsOrigin: CommsOrigin;
|
||||||
|
|
||||||
|
private lastExecutedCommandIds: string[] = new Array(32);
|
||||||
|
private lastExecutedCommandIndex: number = 0;
|
||||||
|
|
||||||
|
private disableTunnel: boolean = false;
|
||||||
|
private popupContext: any = {};
|
||||||
|
|
||||||
|
private iframeForwardingList: {iframe: any, fn: (action, payload, context?) => void}[] = [];
|
||||||
|
|
||||||
|
// private uiUri = window.location.href;
|
||||||
|
|
||||||
|
constructor(options?: {isUWServer?: boolean, name?: string, commsOrigin: CommsOrigin}) {
|
||||||
|
if (!options?.isUWServer) {
|
||||||
|
this.setupIframeTunnelling();
|
||||||
|
}
|
||||||
|
this.name = options?.name ?? '(unnamed EventBus)';
|
||||||
|
this.commsOrigin = options?.commsOrigin;
|
||||||
|
}
|
||||||
|
|
||||||
|
setupPopupTunnelWorkaround(context: EventBusContext): void {
|
||||||
|
this.disableTunnel = true;
|
||||||
|
this.popupContext = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
//#region lifecycle
|
||||||
|
destroy() {
|
||||||
|
this.commands = null;
|
||||||
|
this.destroyIframeTunnelling();
|
||||||
|
}
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
setComms(comms: CommsClient | CommsServer) {
|
||||||
|
this.comms = comms;
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribe(commandString: string, command: EventBusCommand) {
|
||||||
|
if (!this.commands[commandString]) {
|
||||||
|
this.commands[commandString] = [command];
|
||||||
|
} else {
|
||||||
|
this.commands[commandString].push(command);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribeMulti(commands: {[commandString: string]: EventBusCommand}, source?: any) {
|
||||||
|
for (const key in commands) {
|
||||||
|
this.subscribe(
|
||||||
|
key,
|
||||||
|
{
|
||||||
|
...commands[key],
|
||||||
|
source: source ?? commands[key].source
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes all commands from a given source
|
||||||
|
* @param source
|
||||||
|
*/
|
||||||
|
unsubscribeAll(source: any) {
|
||||||
|
for (const commandString in this.commands) {
|
||||||
|
this.commands[commandString] = this.commands[commandString].filter(x => x.source !== source);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
forwardToIframe(iframe: any, fn: (action: string, payload: any, context?: EventBusContext) => void) {
|
||||||
|
this.cancelIframeForwarding(iframe);
|
||||||
|
this.iframeForwardingList.push({iframe, fn});
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelIframeForwarding(iframe: any) {
|
||||||
|
const existingForwarding = this.iframeForwardingList.findIndex((x: any) => x.iframe === iframe);
|
||||||
|
if (existingForwarding !== -1) {
|
||||||
|
this.iframeForwardingList.splice(existingForwarding, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private cloneContext(context: EventBusContext = {}): EventBusContext {
|
||||||
|
return {
|
||||||
|
stopPropagation: context.stopPropagation,
|
||||||
|
origin: context.origin,
|
||||||
|
frameUrl: context.frameUrl,
|
||||||
|
visitedBusses: context.visitedBusses ? [...context.visitedBusses] : undefined,
|
||||||
|
commandId: context.commandId,
|
||||||
|
comms: context.comms ? {
|
||||||
|
forwardTo: context.comms.forwardTo,
|
||||||
|
sourceFrame: context.comms?.sourceFrame ? { ...context.comms?.sourceFrame } : undefined
|
||||||
|
} : undefined,
|
||||||
|
borderCrossings: context.borderCrossings ? { ...context.borderCrossings } : undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
send(command: string, commandData: any, context: EventBusContext = {}) {
|
||||||
|
// context = this.cloneContext(context); // Firefox throws an error if we don't clone the context.
|
||||||
|
|
||||||
|
if (context.visitedBusses?.includes(this.uuid)) {
|
||||||
|
console.warn('this bus was already visited before. Doing nothing.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (context.commandId && this.lastExecutedCommandIds.includes(context.commandId)) {
|
||||||
|
console.warn('this command was already sent');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// we want to avoid re-assigning context.visitedBusses if possible
|
||||||
|
// in order to reduce the amount of garbage that needs to be collected.
|
||||||
|
context.visitedBusses ? context.visitedBusses.push(this.uuid) : context.visitedBusses = [this.uuid];
|
||||||
|
|
||||||
|
// execute commands we have subscriptions for
|
||||||
|
if (this.commands?.[command]) {
|
||||||
|
for (const eventBusCommand of this.commands[command]) {
|
||||||
|
eventBusCommand.function(commandData, context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// preventing messages from flowing back to their original senders is
|
||||||
|
// CommsServer's job. EventBus does not have enough data for this decision.
|
||||||
|
// We do, however, have enough data to prevent backflow of messages that
|
||||||
|
// crossed CommsServer once already.
|
||||||
|
if (!context.commandId) {
|
||||||
|
context.commandId = crypto.randomUUID();
|
||||||
|
}
|
||||||
|
if (context.commandId) {
|
||||||
|
const i = this.lastExecutedCommandIndex++ % this.lastExecutedCommandIds.length;
|
||||||
|
this.lastExecutedCommandIds[i] = context.commandId;
|
||||||
|
}
|
||||||
|
if (!context.origin) {
|
||||||
|
context.origin = this.commsOrigin;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.comms
|
||||||
|
&& ( // ensure each message only enters commsServer once!
|
||||||
|
this.commsOrigin === context.origin // if these two differ, we already sent that message through Comms once,
|
||||||
|
|| this.commsOrigin === CommsOrigin.Server // CommsServer needs to forward everything, otherwise messages stop on
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
this.comms.sendMessage({command, config: commandData, context}, context);
|
||||||
|
} catch (e) {
|
||||||
|
if (command !== 'reload-required') {
|
||||||
|
// We shouldn't let reload-required command to trigger new reload-required commands.
|
||||||
|
this.send('reload-required', {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// call forwarding functions if they exist
|
||||||
|
for (const forwarding of this.iframeForwardingList) {
|
||||||
|
forwarding.fn(
|
||||||
|
command,
|
||||||
|
commandData,
|
||||||
|
{
|
||||||
|
...context,
|
||||||
|
borderCrossings: {
|
||||||
|
...context?.borderCrossings,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// send to parent iframe
|
||||||
|
if (!this.disableTunnel && typeof window !== 'undefined') {
|
||||||
|
window.parent.postMessage(
|
||||||
|
{
|
||||||
|
action: 'uw-bus-tunnel',
|
||||||
|
payload: {command, config: commandData, context} as EventBusMessage
|
||||||
|
},
|
||||||
|
'*'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context?.stopPropagation) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
//#region iframe tunnelling
|
||||||
|
private setupIframeTunnelling() {
|
||||||
|
// forward messages coming from iframe tunnels
|
||||||
|
window.addEventListener('message', this);
|
||||||
|
}
|
||||||
|
private destroyIframeTunnelling() {
|
||||||
|
window.removeEventListener('message', this);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Handles 'message' events (formerly handleIframeMessage)
|
||||||
|
* @param event
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
handleEvent(event: any) {
|
||||||
|
if (event.data?.action !== 'uw-bus-tunnel') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = event.data.payload as EventBusMessage;
|
||||||
|
|
||||||
|
if (!payload.context?.visitedBusses) {
|
||||||
|
// this should never be visible to a real user
|
||||||
|
console.warn('Received iframe message without context. Doing nothing in order to avoid infinite loop. Event:', event);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payload.context?.visitedBusses?.includes(this.uuid)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.send(payload.command, payload.config, payload.context);
|
||||||
|
}
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
}
|
||||||
@ -1,7 +1,6 @@
|
|||||||
import { decycle } from 'json-cyclic';
|
import { decycle } from 'json-cyclic';
|
||||||
import Comms from './comms/Comms';
|
import Comms from './comms/Comms';
|
||||||
import BrowserDetect from '../conf/BrowserDetect';
|
import BrowserDetect from '../conf/BrowserDetect';
|
||||||
import { browser } from 'webextension-polyfill-ts';
|
|
||||||
|
|
||||||
if (process.env.CHANNEL !== 'stable'){
|
if (process.env.CHANNEL !== 'stable'){
|
||||||
console.info('Loading Logger');
|
console.info('Loading Logger');
|
||||||
@ -22,16 +21,16 @@ export const baseLoggingOptions: LoggerConfig = {
|
|||||||
"settings": true,
|
"settings": true,
|
||||||
"keyboard": true,
|
"keyboard": true,
|
||||||
"mousemove": false,
|
"mousemove": false,
|
||||||
"actionHandler": true,
|
"kbmHandler": true,
|
||||||
"comms": true,
|
"comms": true,
|
||||||
"playerDetect": true,
|
"playerDetect": true,
|
||||||
"resizer": true,
|
"resizer": true,
|
||||||
"scaler": true,
|
"scaler": true,
|
||||||
"stretcher": true,
|
"stretcher": true,
|
||||||
// "videoRescan": true,
|
"videoRescan": false,
|
||||||
// "playerRescan": true,
|
"playerRescan": false,
|
||||||
"arDetect": true,
|
"arDetect": true,
|
||||||
"arDetect_verbose": true
|
"arDetect_verbose": false
|
||||||
},
|
},
|
||||||
allowBlacklistedOrigins: {
|
allowBlacklistedOrigins: {
|
||||||
'periodicPlayerCheck': false,
|
'periodicPlayerCheck': false,
|
||||||
@ -47,7 +46,7 @@ export interface LoggingOptions {
|
|||||||
settings?: boolean;
|
settings?: boolean;
|
||||||
keyboard?: boolean;
|
keyboard?: boolean;
|
||||||
mousemove?: boolean;
|
mousemove?: boolean;
|
||||||
actionHandler?: boolean;
|
kbmHandler?: boolean;
|
||||||
comms?: boolean;
|
comms?: boolean;
|
||||||
playerDetect?: boolean;
|
playerDetect?: boolean;
|
||||||
resizer?: boolean;
|
resizer?: boolean;
|
||||||
@ -91,20 +90,26 @@ class Logger {
|
|||||||
constructor(options?: {vuexStore?: any, uwInstance?: any}) {
|
constructor(options?: {vuexStore?: any, uwInstance?: any}) {
|
||||||
this.vuexStore = options?.vuexStore;
|
this.vuexStore = options?.vuexStore;
|
||||||
this.uwInstance = options?.uwInstance;
|
this.uwInstance = options?.uwInstance;
|
||||||
|
|
||||||
|
chrome.storage.onChanged.addListener((changes, area) => {
|
||||||
|
this.storageChangeListener(changes, area)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
static saveConfig(conf: LoggerConfig) {
|
static saveConfig(conf: LoggerConfig) {
|
||||||
|
console.warn('LEGACY LOGGER IS STILL BEING CALLED FROM SOMEWHERE!', new Error().stack);
|
||||||
|
|
||||||
if (process.env.CHANNEL === 'dev') {
|
if (process.env.CHANNEL === 'dev') {
|
||||||
console.info('Saving logger conf:', conf)
|
console.info('Saving logger conf:', conf)
|
||||||
}
|
}
|
||||||
|
|
||||||
browser.storage.local.set( {'uwLogger': JSON.stringify(conf)});
|
chrome.storage.local.set( {'uwLogger': JSON.stringify(conf)});
|
||||||
}
|
}
|
||||||
|
|
||||||
static syncConfig(callback: (x) => void) {
|
static syncConfig(callback: (x) => void) {
|
||||||
browser.storage.onChanged.addListener( (changes, area) => {
|
chrome.storage.onChanged.addListener( (changes: any, area: string) => {
|
||||||
if (changes.uwLogger) {
|
if (changes.uwLogger) {
|
||||||
const newLoggerConf = JSON.parse(changes.uwLogger.newValue)
|
const newLoggerConf = JSON.parse(changes.uwLogger.newValue as any)
|
||||||
if (process.env.CHANNEL === 'dev') {
|
if (process.env.CHANNEL === 'dev') {
|
||||||
console.info('Logger settings reloaded. New conf:', newLoggerConf);
|
console.info('Logger settings reloaded. New conf:', newLoggerConf);
|
||||||
}
|
}
|
||||||
@ -117,10 +122,10 @@ class Logger {
|
|||||||
let ret;
|
let ret;
|
||||||
|
|
||||||
// if (BrowserDetect.firefox) {
|
// if (BrowserDetect.firefox) {
|
||||||
ret = await browser.storage.local.get('uwLogger');
|
ret = await chrome.storage.local.get('uwLogger');
|
||||||
// } else if (BrowserDetect.anyChromium) {
|
// } else if (BrowserDetect.anyChromium) {
|
||||||
// ret = await new Promise( (resolve, reject) => {
|
// ret = await new Promise( (resolve, reject) => {
|
||||||
// browser.storage.local.get('uwLogger', (res) => resolve(res));
|
// chrome.storage.local.get('uwLogger', (res) => resolve(res));
|
||||||
// });
|
// });
|
||||||
// }
|
// }
|
||||||
|
|
||||||
@ -162,12 +167,12 @@ class Logger {
|
|||||||
if (this.conf.fileOptions === undefined) {
|
if (this.conf.fileOptions === undefined) {
|
||||||
this.conf.fileOptions = {};
|
this.conf.fileOptions = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
this.startTime = performance.now();
|
this.startTime = performance.now();
|
||||||
this.temp_disable = false;
|
this.temp_disable = false;
|
||||||
this.stopTime = this.conf.timeout ? performance.now() + (this.conf.timeout * 1000) : undefined;
|
this.stopTime = this.conf.timeout ? performance.now() + (this.conf.timeout * 1000) : undefined;
|
||||||
|
|
||||||
browser.storage.onChanged.addListener( (changes, area) => {
|
chrome.storage.onChanged.addListener( (changes: any, area: any) => {
|
||||||
if (process.env.CHANNEL === 'dev') {
|
if (process.env.CHANNEL === 'dev') {
|
||||||
if (!changes.uwLogger) {
|
if (!changes.uwLogger) {
|
||||||
// console.info('[Logger::<storage/on change> No new logger settings!');
|
// console.info('[Logger::<storage/on change> No new logger settings!');
|
||||||
@ -190,10 +195,37 @@ class Logger {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
storageChangeListener(changes: any, area: any) {
|
||||||
|
if (!changes.uwLogger) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.conf = JSON.parse(changes.uwLogger.newValue);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[uwLogger] Error while trying to parse new conf for logger:', e, '\nWe received the following changes:', changes, 'for area:', area);
|
||||||
|
}
|
||||||
|
|
||||||
|
// This code can only execute if user tried to enable or disable logging
|
||||||
|
// through the popup. In cases like this, we do not gate the console.log
|
||||||
|
// behind a check, since we _always_ want to have this feedback in response
|
||||||
|
// to an action.
|
||||||
|
console.info(
|
||||||
|
'[uwLogger] logger config changed! New configuration:',
|
||||||
|
this.conf, '\nraw changes:', changes, 'area?', area,
|
||||||
|
'\n————————————————————————————————————————————————————————————————————————\n\n\n\n\n\n\n\n\n\n\n\n-----\nLogging with new settings starts now.'
|
||||||
|
);
|
||||||
|
|
||||||
|
// initiate loger if need be
|
||||||
|
if (!this.startTime) {
|
||||||
|
this.init(this.conf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setVuexStore(store) {
|
setVuexStore(store) {
|
||||||
this.vuexStore = store;
|
this.vuexStore = store;
|
||||||
}
|
}
|
||||||
|
|
||||||
clear() {
|
clear() {
|
||||||
this.history = [];
|
this.history = [];
|
||||||
this.startTime = performance.now();
|
this.startTime = performance.now();
|
||||||
@ -207,12 +239,12 @@ class Logger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// async getSaved() {
|
// async getSaved() {
|
||||||
// return Logger.getSaved();
|
// return Logger.getSaved();
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
|
||||||
// allow syncing of start times between bg and page scripts.
|
// allow syncing of start times between bg and page scripts.
|
||||||
// may result in negative times in the log file, but that doesn't
|
// may result in negative times in the log file, but that doesn't
|
||||||
// really matter
|
// really matter
|
||||||
getStartTime() {
|
getStartTime() {
|
||||||
return this.startTime;
|
return this.startTime;
|
||||||
@ -236,8 +268,18 @@ class Logger {
|
|||||||
// return logfileStr;
|
// return logfileStr;
|
||||||
// }
|
// }
|
||||||
getFileLogJSONString() {
|
getFileLogJSONString() {
|
||||||
|
let site;
|
||||||
|
|
||||||
|
// NOTE: no more window object on UWServer side of things!
|
||||||
|
// (or rather, we could get it, but we don't care enough to get it in this instance)
|
||||||
|
try {
|
||||||
|
site = window && window.location;
|
||||||
|
} catch {
|
||||||
|
site = 'uw-bg';
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
site: window && window.location,
|
site,
|
||||||
log: JSON.stringify(this.history),
|
log: JSON.stringify(this.history),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -254,7 +296,7 @@ class Logger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// this should be used mostly in background page instance of logger, btw
|
// this should be used mostly in background page instance of logger, btw
|
||||||
//
|
//
|
||||||
addToGlobalHistory(key, log) {
|
addToGlobalHistory(key, log) {
|
||||||
this.globalHistory[key] = log;
|
this.globalHistory[key] = log;
|
||||||
this.log('info', 'debug', 'Added log for', key, 'to global history. Current history:', this.globalHistory);
|
this.log('info', 'debug', 'Added log for', key, 'to global history. Current history:', this.globalHistory);
|
||||||
@ -326,8 +368,8 @@ class Logger {
|
|||||||
stackInfo.stack.trace.splice(i);
|
stackInfo.stack.trace.splice(i);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return stackInfo;
|
return stackInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -379,14 +421,14 @@ class Logger {
|
|||||||
|
|
||||||
if (this.isBlacklistedOrigin(stackInfo)) {
|
if (this.isBlacklistedOrigin(stackInfo)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// if either of these two is true, we allow logging to happen (forbidden origins were checked above)
|
// if either of these two is true, we allow logging to happen (forbidden origins were checked above)
|
||||||
return (this.canLogFile(component) || this.canLogConsole(component) || stackInfo.exitLogs);
|
return (this.canLogFile(component) || this.canLogConsole(component) || stackInfo.exitLogs);
|
||||||
}
|
}
|
||||||
|
|
||||||
canLogFile(component) {
|
canLogFile(component) {
|
||||||
if (!this.conf.fileOptions.enabled || this.temp_disable) {
|
if (!this.conf?.fileOptions?.enabled || this.temp_disable) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (Array.isArray(component) && component.length ) {
|
if (Array.isArray(component) && component.length ) {
|
||||||
@ -416,7 +458,7 @@ class Logger {
|
|||||||
|
|
||||||
return this.conf.logAll;
|
return this.conf.logAll;
|
||||||
}
|
}
|
||||||
|
|
||||||
logToFile(message, stackInfo) {
|
logToFile(message, stackInfo) {
|
||||||
let ts = performance.now();
|
let ts = performance.now();
|
||||||
if (ts <= this.history[this.history.length - 1]) {
|
if (ts <= this.history[this.history.length - 1]) {
|
||||||
@ -433,16 +475,16 @@ class Logger {
|
|||||||
logToConsole(level, message, stackInfo) {
|
logToConsole(level, message, stackInfo) {
|
||||||
try {
|
try {
|
||||||
switch (level) {
|
switch (level) {
|
||||||
case 'error':
|
case 'error':
|
||||||
console.error(...message, {stack: stackInfo});
|
console.error(...message, {stack: stackInfo});
|
||||||
break;
|
break;
|
||||||
case 'warn':
|
case 'warn':
|
||||||
console.warn(...message, {stack: stackInfo});
|
console.warn(...message, {stack: stackInfo});
|
||||||
break;
|
break;
|
||||||
case 'info':
|
case 'info':
|
||||||
console.info(...message, {stack: stackInfo});
|
console.info(...message, {stack: stackInfo});
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
console.log(...message, {stack: stackInfo});
|
console.log(...message, {stack: stackInfo});
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -480,7 +522,7 @@ class Logger {
|
|||||||
if (this.isBlacklistedOrigin(stackInfo)) {
|
if (this.isBlacklistedOrigin(stackInfo)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.conf.fileOptions?.enabled) {
|
if (this.conf.fileOptions?.enabled) {
|
||||||
if (this.canLogFile(component) || stackInfo.exitLogs) {
|
if (this.canLogFile(component) || stackInfo.exitLogs) {
|
||||||
this.logToFile(message, stackInfo);
|
this.logToFile(message, stackInfo);
|
||||||
@ -513,7 +555,7 @@ class Logger {
|
|||||||
message: "-------------------------------------- CAHEN --------------------------------------"
|
message: "-------------------------------------- CAHEN --------------------------------------"
|
||||||
});
|
});
|
||||||
|
|
||||||
// find the spot for the half second mark. In this case, we don't really particularly care whether timestamps
|
// find the spot for the half second mark. In this case, we don't really particularly care whether timestamps
|
||||||
// are duped due to cahen warnings
|
// are duped due to cahen warnings
|
||||||
while (this.history[i--].ts > halfSecondMark) {}
|
while (this.history[i--].ts > halfSecondMark) {}
|
||||||
this.history.push({
|
this.history.push({
|
||||||
@ -561,7 +603,7 @@ class Logger {
|
|||||||
|
|
||||||
Comms.sendMessage({cmd: 'show-logger', forwardToSameFramePort: true, port: 'content-ui-port'});
|
Comms.sendMessage({cmd: 'show-logger', forwardToSameFramePort: true, port: 'content-ui-port'});
|
||||||
|
|
||||||
let exportObject;
|
let exportObject;
|
||||||
try {
|
try {
|
||||||
exportObject = {
|
exportObject = {
|
||||||
pageLogs: decycle(this.history),
|
pageLogs: decycle(this.history),
|
||||||
@ -599,7 +641,7 @@ class Logger {
|
|||||||
|
|
||||||
console.info('[info] vuex store present. Parsing logs.');
|
console.info('[info] vuex store present. Parsing logs.');
|
||||||
|
|
||||||
let exportObject;
|
let exportObject;
|
||||||
try {
|
try {
|
||||||
exportObject = {
|
exportObject = {
|
||||||
pageLogs: decycle(this.history),
|
pageLogs: decycle(this.history),
|
||||||
74
src/ext/module/UWGlobals.js
Normal file
74
src/ext/module/UWGlobals.js
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
export default class UWGlobals {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.videos = [];
|
||||||
|
this.busSubscriptions = [];
|
||||||
|
this.actionSubscriptions = {};
|
||||||
|
this.bus = {
|
||||||
|
sendMessage: (action, config) => this.propagateMessages(action, config),
|
||||||
|
subscribe: this.subscribeToAny,
|
||||||
|
subscribeToAction: this.subscribeToAction
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getNewVideoID() {
|
||||||
|
let random;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
// 4-digit [a-z0-9] string. Should be unique per page on first try
|
||||||
|
random = (Math.random() * 1679616).toFixed().toString(36);
|
||||||
|
|
||||||
|
if (this.videos.findIndex(x => x.vdid === random) === -1) {
|
||||||
|
return random;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addVideo(video) {
|
||||||
|
// get video ID
|
||||||
|
const id = this.getNewVideoID();
|
||||||
|
video.vdid = id;
|
||||||
|
this.videos.push(video);
|
||||||
|
}
|
||||||
|
|
||||||
|
getVideo(id) {
|
||||||
|
return this.videos.find(x => x.vdid === id);
|
||||||
|
}
|
||||||
|
|
||||||
|
importSubscriptionsFromCommsHandlers(commands) {
|
||||||
|
for (const action in commands) {
|
||||||
|
for (const command of commands[action]) {
|
||||||
|
this.subscribeToAction(action, command);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribeToAction(action, callback) {
|
||||||
|
if (!this.actionSubscriptions[action]) {
|
||||||
|
this.actionSubscriptions[action] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.actionSubscriptions[action].push(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribeToAny(callback) {
|
||||||
|
this.busSubscriptions.push(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
propagateMessages(action, config) {
|
||||||
|
if (this.busSubscriptions) {
|
||||||
|
for (const subscription of this.busSubscriptions) {
|
||||||
|
subscription(action, config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.actionSubscriptions && this.actionSubscriptions[action]) {
|
||||||
|
for (const subscription of this.actionSubscriptions[action]) {
|
||||||
|
subscription(config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
// todo: implement
|
||||||
|
}
|
||||||
|
}
|
||||||
1439
src/ext/module/aard/Aard.ts
Normal file
1439
src/ext/module/aard/Aard.ts
Normal file
File diff suppressed because it is too large
Load Diff
390
src/ext/module/aard/AardDebugUi.ts
Normal file
390
src/ext/module/aard/AardDebugUi.ts
Normal file
@ -0,0 +1,390 @@
|
|||||||
|
import { Aard } from './Aard';
|
||||||
|
import { AardLegacy } from './AardLegacy';
|
||||||
|
import { AardPerformanceData } from './AardTimers';
|
||||||
|
import { FallbackCanvas } from './gl/FallbackCanvas';
|
||||||
|
|
||||||
|
export class AardDebugUi {
|
||||||
|
|
||||||
|
aard: any;
|
||||||
|
|
||||||
|
uiAnchorElement: HTMLDivElement;
|
||||||
|
pauseOnArCheck: boolean = false;
|
||||||
|
|
||||||
|
uiVisibility: any = {};
|
||||||
|
|
||||||
|
constructor(aard: any) {
|
||||||
|
this.aard = aard;
|
||||||
|
|
||||||
|
this.uiVisibility = {
|
||||||
|
detectionDetails: aard.settings.active.ui.dev.aardDebugOverlay.showDetectionDetails
|
||||||
|
};
|
||||||
|
|
||||||
|
(window as any).ultrawidify_uw_aard_debug_tools = {
|
||||||
|
enableStopOnChange: () => this.changePauseOnCheck(true),
|
||||||
|
disableStopOnChange: () => this.changePauseOnCheck(false),
|
||||||
|
resumeVideo: () => this.resumeVideo(),
|
||||||
|
step: () => this.aard.step()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initContainer() {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.id = 'uw-aard-debug-ui-container';
|
||||||
|
div.innerHTML = `
|
||||||
|
<div style="
|
||||||
|
position: fixed; top: 0; left: 0; width: 100vw; height: 100dvh; display: flex; flex-direction: column; pointer-events: none; z-index: 9999; font-size: 16px; font-family: 'Overpass Mono', monospace;
|
||||||
|
">
|
||||||
|
<div style="width: 100%; display: flex; flex-direction: row; justify-content: space-between; backdrop-filter: blur(0.5rem) brightness(0.5);">
|
||||||
|
<div style="padding: 1rem; color: #fff">
|
||||||
|
<h1>Aard debug overlay</h1>
|
||||||
|
</div>
|
||||||
|
<div style="pointer-events: all; display: flex; flex-direction: column; margin-right: 1rem;">
|
||||||
|
<button id="uw-aard-debug_show-detection-details">Show det. details</button>
|
||||||
|
<button id="uw-aard-debug_hide-detection-details">Hide det. details</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
#uw-aard-debug_performance-container #uw-aard-debug_performance-popup {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
#uw-aard-debug_performance-container:hover #uw-aard-debug_performance-popup {
|
||||||
|
display: block;
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
top: 3rem;
|
||||||
|
width: 100vw;
|
||||||
|
|
||||||
|
color: #aaa;
|
||||||
|
background-color: #000;
|
||||||
|
border: 2px solid #fa6;
|
||||||
|
padding: 16px;
|
||||||
|
padding-right: 32px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
backdrop-filter: blur(1rem) brightness(0.5);
|
||||||
|
|
||||||
|
pointer-events: none;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div id="uw-aard-debug_performance-container" style="flex-grow: 1; position: relative; pointer-events: auto;">
|
||||||
|
<div id="uw-aard-debug_performance-mini" style="width: 100%; color: #aaa; display: flex; flex-direction: column; font-size: 12px;"></div>
|
||||||
|
<div id="uw-aard-debug_performance-popup" style="position: fixed; top: 3rem; z-index: 3000;">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="pointer-events: all">
|
||||||
|
<button id="uw-aard-debug-ui_close-overlay">Close overlay</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div id="uw-aard-debug-ui_body" style="display: flex; flex-direction: row; width: 100%; margin-top: 8rem;">
|
||||||
|
<div style="">
|
||||||
|
<div id="uw-aard-debug_aard-sample-canvas" style="min-width: 640px"></div>
|
||||||
|
<div style="background: black; color: #fff"; font-size: 24px;">AARD IN</div>
|
||||||
|
|
||||||
|
<div style="pointer-events: all">
|
||||||
|
<button id="uw-aard-debug-ui_enable-stop-on-change" style="">Pause video on aspect ratio change</button>
|
||||||
|
<button id="uw-aard-debug-ui_disable-stop-on-change" style="display: none">Stop pausing video on aspect ratio change</button>
|
||||||
|
<button id="uw-aard-debug-ui_resume-video" >Resume video</button>
|
||||||
|
<button id="uw-aard-debug-ui_enable-step" >Run ARD once</button>
|
||||||
|
<button id="uw-aard-debug-ui_enable-step-nocache" >Run ARD (bypass cache)</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="flex-grow: 1"></div>
|
||||||
|
<div>
|
||||||
|
<div style="background: black; color: #ccc;">
|
||||||
|
<div style="font-size: 24px; padding: 1rem;">
|
||||||
|
Debug results:
|
||||||
|
</div>
|
||||||
|
<pre id="uw-aard-results"></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="width: 1920px; border: 2px dotted #142; margin-right:2rem;">
|
||||||
|
<div id="uw-aard-debug_aard-output" style="zoom: 3; image-rendering: pixelated;"></div>
|
||||||
|
<div style="background: black; color: #fff; font-size: 24px;">AARD RESULT</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(div);
|
||||||
|
this.uiAnchorElement = div;
|
||||||
|
|
||||||
|
document.getElementById('uw-aard-debug-ui_enable-stop-on-change').onclick = () => this.changePauseOnCheck(true);
|
||||||
|
document.getElementById('uw-aard-debug-ui_disable-stop-on-change').onclick = () => this.changePauseOnCheck(false);
|
||||||
|
document.getElementById('uw-aard-debug-ui_resume-video').onclick = () => this.resumeVideo();
|
||||||
|
document.getElementById('uw-aard-debug-ui_enable-step').onclick = () => this.aard.step();
|
||||||
|
document.getElementById('uw-aard-debug-ui_enable-step-nocache').onclick = () => this.aard.step({noCache: true});
|
||||||
|
document.getElementById('uw-aard-debug-ui_close-overlay').onclick = () => (this.aard as any).hideDebugCanvas();
|
||||||
|
document.getElementById('uw-aard-debug_show-detection-details').onclick = () => {this.uiVisibility.detectionDetails = true; this.setOverlayVisibility();};
|
||||||
|
document.getElementById('uw-aard-debug_hide-detection-details').onclick = () => {this.uiVisibility.detectionDetails = false; this.setOverlayVisibility();};
|
||||||
|
|
||||||
|
this.setOverlayVisibility();
|
||||||
|
}
|
||||||
|
|
||||||
|
changePauseOnCheck(pauseOnChange: boolean) {
|
||||||
|
this.pauseOnArCheck = pauseOnChange;
|
||||||
|
|
||||||
|
document.getElementById("uw-aard-debug-ui_enable-stop-on-change").style.display = pauseOnChange ? "none" : "";
|
||||||
|
document.getElementById("uw-aard-debug-ui_disable-stop-on-change").style.display = pauseOnChange ? "" : "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
destroyContainer() {
|
||||||
|
this.uiAnchorElement.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
attachCanvases(sample: HTMLCanvasElement, debug: HTMLCanvasElement) {
|
||||||
|
const sampleCanvasParent = document.getElementById('uw-aard-debug_aard-sample-canvas');
|
||||||
|
sampleCanvasParent.appendChild(sample);
|
||||||
|
const debugCanvasParent = document.getElementById('uw-aard-debug_aard-output');
|
||||||
|
debugCanvasParent.appendChild(debug);
|
||||||
|
}
|
||||||
|
|
||||||
|
resumeVideo() {
|
||||||
|
(this.aard as any).video.play();
|
||||||
|
this.aard.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private updatePerformanceResults() {
|
||||||
|
const previewDiv = document.getElementById('uw-aard-debug_performance-mini');
|
||||||
|
const popupDiv = document.getElementById('uw-aard-debug_performance-popup');
|
||||||
|
|
||||||
|
const previewContent = `
|
||||||
|
<div style="display: flex; flex-direction: row">
|
||||||
|
<div style="width: 120px">Current:</div>
|
||||||
|
<div style="flex-grow: 1;">${this.generateMiniGraphBar(this.aard.timer.current)}</div>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; flex-direction: row">
|
||||||
|
<div style="width: 120px">Average:</div>
|
||||||
|
<div style="flex-grow: 1">${this.generateMiniGraphBar(this.aard.timer.average)}</div>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; flex-direction: row">
|
||||||
|
<div style="width: 120px">Last chg.:</div>
|
||||||
|
<div></div>
|
||||||
|
<div style="flex-grow: 1">${this.generateMiniGraphBar(this.aard.timer.lastChange)}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const popupContent = `
|
||||||
|
<h2 style="color: #fa6; margin-bottom: 1rem">Detailed performance analysis:</h2>
|
||||||
|
<div>
|
||||||
|
<span style="color: #fff">About:</span><br/>
|
||||||
|
Aard version: <span style="color: #fa6">${this.aard instanceof AardLegacy ? 'legacy' : this.aard instanceof Aard ? 'experimental' : 'unknown'}</span> <small>(instanceof AardLegacy? ${this.aard instanceof AardLegacy}, Aard? ${this.aard instanceof Aard})</small><br/>
|
||||||
|
canvas type: <span style="color: #fa6">${!this.aard.canvasStore.main ? '<canvas not initialized>' : this.aard.canvasStore.main instanceof FallbackCanvas ? '2dCanvas' : 'webgl'}</span> |
|
||||||
|
legacy default: <span style="color: #fa6">${this.aard.settings.active.aardLegacy.aardType}</span>,
|
||||||
|
experimental default: <span style="color: #fa6">${this.aard.settings.active.aard.aardType}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
<div style="width: 100%; display: flex; flex-direction: column">
|
||||||
|
<div style="margin-bottom: 1rem;">
|
||||||
|
${this.generateRawTimes(this.aard.timer.current)}
|
||||||
|
</div>
|
||||||
|
<div style="color: #fff">Stage times (not cumulative):</div>
|
||||||
|
<div style="display: flex; flex-direction: row; width: 100%; height: 150px">
|
||||||
|
<div style="width: 160px; text-align: right; padding-right: 4px;">Current:</div>
|
||||||
|
<div style="flex-grow: 1;">${this.generateMiniGraphBar(this.aard.timer.current, true)}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-bottom: 1rem;">
|
||||||
|
${this.generateRawTimes(this.aard.timer.average)}
|
||||||
|
</div>
|
||||||
|
<div style="color: #fff">Stage times (not cumulative):</div>
|
||||||
|
<div style="display: flex; flex-direction: row; width: 100%; height: 150px">
|
||||||
|
<div style="width: 160px; text-align: right; padding-right: 4px;">Average:</div>
|
||||||
|
<div style="flex-grow: 1;">${this.generateMiniGraphBar(this.aard.timer.average, true)}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-bottom: 1rem;">
|
||||||
|
${this.generateRawTimes(this.aard.timer.lastChange)}
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; flex-direction: row; width: 100%; height: 150px">
|
||||||
|
<div style="width: 160px; text-align: right; padding-right: 4px;">Last change:</div>
|
||||||
|
<div style="flex-grow: 1;">${this.generateMiniGraphBar(this.aard.timer.lastChange, true)}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- <pre>${JSON.stringify({current: this.aard.timer.current, average: this.aard.timer.average, lastChange: this.aard.timer.lastChange}, null, 2)}</pre> -->
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
|
||||||
|
previewDiv.innerHTML = previewContent;
|
||||||
|
popupDiv.innerHTML = popupContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getBarLabel(width: number, leftOffset: number, topOffset: number, label: string, detailed: boolean, extraStyles?: string) {
|
||||||
|
if (!detailed) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
let offsets: string;
|
||||||
|
let text: string = '';
|
||||||
|
|
||||||
|
if (leftOffset + width < 80) {
|
||||||
|
// at the end of the bar
|
||||||
|
offsets = `left: ${leftOffset + width}%;`;
|
||||||
|
} else {
|
||||||
|
if (width < 15 && leftOffset < 100) {
|
||||||
|
// before the bar
|
||||||
|
offsets = `right: ${100 - leftOffset}%;`;
|
||||||
|
text = 'color: #fff;';
|
||||||
|
} else {
|
||||||
|
// inside the bar, aligned to right
|
||||||
|
offsets = `right: ${Math.max(100 - (leftOffset + width), 0)}%;`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div style="
|
||||||
|
position: absolute;
|
||||||
|
${offsets} top: ${topOffset}px;
|
||||||
|
padding-left: 0.5rem; padding-right: 0.5rem;đ
|
||||||
|
${text}
|
||||||
|
text-shadow: 0 0 2px #000, 0 0 2px #000; 0 0 2px #000; 0 0 2px #000;
|
||||||
|
${extraStyles}
|
||||||
|
">
|
||||||
|
${label}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateRawTimes(timer) {
|
||||||
|
return `
|
||||||
|
<span style="color: #fff">Raw times (cumulative; -1 = test was skipped):</span><br/>
|
||||||
|
draw <span style="color: #fa6; margin-right: 0.5rem;">${timer.draw.toFixed(2)}ms</span>
|
||||||
|
get data <span style="color: #fa6; margin-right: 0.5rem;">${timer.getImage.toFixed(2)}ms</span>
|
||||||
|
black lv. <span style="color: #fa6; margin-right: 0.5rem;">${timer.fastBlackLevel.toFixed(2)}ms</span>
|
||||||
|
guard/image <span style="color: #fa6; margin-right: 0.5rem;">${timer.guardLine.toFixed(3)}ms</span>
|
||||||
|
edge <span style="color: #fa6; margin-right: 0.5rem;">${timer.edgeScan.toFixed(2)}ms</span>
|
||||||
|
gradient <span style="color: #fa6; margin-right: 0.5rem;">${timer.gradient.toFixed(3)}ms</span>
|
||||||
|
post <span style="color: #fa6; margin-right: 0.5rem;">${timer.scanResults.toFixed(2)}ms</span>
|
||||||
|
subtitle <span style="color: #fa6; margin-right: 0.5rem;">${timer.subtitleScan.toFixed(2)}ms</span>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateMiniGraphBar(perf: AardPerformanceData, detailed: boolean = false) {
|
||||||
|
if (!perf) {
|
||||||
|
return `
|
||||||
|
n/a
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let total = 0;
|
||||||
|
|
||||||
|
const draw = Math.max(perf.draw, 0);
|
||||||
|
total += draw;
|
||||||
|
|
||||||
|
const getImageStart = draw;
|
||||||
|
const getImage = Math.max(perf.getImage - total, 0);
|
||||||
|
total += getImage;
|
||||||
|
|
||||||
|
const fastBlackLevelStart = getImageStart + getImage;
|
||||||
|
const fastBlackLevel = Math.max(perf.fastBlackLevel - total, 0);
|
||||||
|
total += fastBlackLevel;
|
||||||
|
|
||||||
|
const guardLineStart = fastBlackLevelStart + fastBlackLevel;
|
||||||
|
const guardLine = (perf.guardLine !== undefined && perf.guardLine !== -1) ? Math.max(perf.guardLine - total, 0) : 0;
|
||||||
|
total += guardLine;
|
||||||
|
|
||||||
|
const edgeScanStart = guardLineStart + guardLine;
|
||||||
|
const edgeScan = (perf.edgeScan !== undefined && perf.edgeScan !== -1) ? Math.max(perf.edgeScan - total, 0) : 0;
|
||||||
|
total += edgeScan;
|
||||||
|
|
||||||
|
const gradientStart = edgeScanStart + edgeScan;
|
||||||
|
const gradient = (perf.gradient !== undefined && perf.gradient !== -1) ? Math.max(perf.gradient - total, 0) : 0;
|
||||||
|
total += gradient;
|
||||||
|
|
||||||
|
const scanResultsStart = gradientStart + gradient;
|
||||||
|
const scanResults = (perf.scanResults !== undefined && perf.scanResults !== -1) ? Math.max(perf.scanResults - total, 0) : 0;
|
||||||
|
total += scanResults;
|
||||||
|
|
||||||
|
const subtitleScanStart = scanResultsStart + scanResults;
|
||||||
|
const subtitleScan = Math.max(perf.subtitleScan - total, 0);
|
||||||
|
total += subtitleScan;
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div style="width: calc(100% - 2rem); position: relative; text-align: right;">
|
||||||
|
${detailed ? '' : `${total.toFixed()} ms`}
|
||||||
|
<div style="position: absolute; top: 0; left: 0; width: ${total}%; background: #fff; height: 2px;"></div>
|
||||||
|
${detailed ? `<div style="position: absolute; top: 0; left: ${total}%; height: 100px; width: 1px; border-left: 1px dotted rgba(255,255,255,0.5)"></div> ` : ''}
|
||||||
|
|
||||||
|
<div style="position: absolute; top: ${detailed ? '2px' : '2px'}; left: 0; min-width: 1px; width: ${draw}%; background: #007; height: 12px;"></div>
|
||||||
|
${this.getBarLabel(draw, 0, 2, `draw: ${draw.toFixed(2)} ms`, detailed)}
|
||||||
|
|
||||||
|
<div style="position: absolute; top: ${detailed ? '14px' : '2px'}; left: ${getImageStart}%; min-width: 1px; width: ${getImage}%; background: #00a; height: 12px;"></div>
|
||||||
|
${this.getBarLabel(getImage, getImageStart, 14, `get data: ${getImage.toFixed(2)} ms`, detailed)}
|
||||||
|
|
||||||
|
<div style="position: absolute; top: ${detailed ? '26px' : '2px'}; left: ${fastBlackLevelStart}%; min-width: 1px; width: ${fastBlackLevel}%; background: #03a; height: 12px;"></div>
|
||||||
|
${this.getBarLabel(fastBlackLevel, fastBlackLevelStart, 26, `black level: ${fastBlackLevel.toFixed(2)} ms`, detailed)};
|
||||||
|
|
||||||
|
<div style="position: absolute; top: ${detailed ? '38px' : '2px'}; left: ${guardLineStart}%; min-width: 1px; width: ${guardLine}%; background: #0f3; height: 12px;"></div>
|
||||||
|
${this.getBarLabel(guardLine, guardLineStart, 38, `guard/image line: ${guardLine.toFixed(2)} ms`, detailed)}
|
||||||
|
|
||||||
|
<div style="position: absolute; top: ${detailed ? '50px' : '2px'}; left: ${edgeScanStart}%; min-width: 1px; width: ${edgeScan}%; background: #f51; height: 12px;"></div>
|
||||||
|
${this.getBarLabel(edgeScan, edgeScanStart, 50, `edge scan (/w validation): ${edgeScan.toFixed(2)} ms`, detailed)}
|
||||||
|
|
||||||
|
<div style="position: absolute; top: ${detailed ? '62px' : '2px'}; left: ${gradientStart}%; min-width: 1px; width: ${gradient}%; background: #fa6; height: 12px;"></div>
|
||||||
|
${this.getBarLabel(gradient, gradientStart, 62, `gradient: ${gradient.toFixed(2)} ms`, detailed)}
|
||||||
|
|
||||||
|
<div style="position: absolute; top: ${detailed ? '74px' : '2px'}; left: ${scanResultsStart}%; min-width: 1px; width: ${scanResults}%; background: #80f; height: 12px;"></div>
|
||||||
|
${this.getBarLabel(scanResults, scanResultsStart, 74, `scan results processing: ${scanResults.toFixed(2)} ms`, detailed)}
|
||||||
|
|
||||||
|
<div style="position: absolute; top: ${detailed ? '86px' : '2px'}; left: ${subtitleScanStart}%; min-width: 1px; width: ${subtitleScan}%; background: rgba(234, 204, 84, 1); height: 12px;"></div>
|
||||||
|
${this.getBarLabel(subtitleScan, subtitleScanStart, 86, `subtitle scan: ${subtitleScan.toFixed(2)} ms`, detailed)}
|
||||||
|
|
||||||
|
${this.getBarLabel(0, subtitleScan + subtitleScanStart, 98, `total: ${total.toFixed(2)} ms`, detailed, 'color: #fff;')}
|
||||||
|
|
||||||
|
<!-- 60/30 fps markers -->
|
||||||
|
<div style="position: absolute; top: ${detailed ? '-12px' : '0'}; left: 16.666%; width: 1px; border-left: 1px dashed #4f9; height: ${detailed ? '112px' : '12px'}; padding-left: 2px; background-color: rgba(0,0,0,0.5); z-index: ${detailed ? '5' : '2'}000;">60fps</div>
|
||||||
|
<div style="position: absolute; top: ${detailed ? '-12px' : '0'}; left: 33.333%; width: 1px; border-left: 1px dashed #ff0; height: ${detailed ? '112px' : '12px'}; padding-left: 2px; background-color: #000; z-index: ${detailed ? '5' : '2'}000;">30fps</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
_lastAr: undefined;
|
||||||
|
updateTestResults(testResults, timers) {
|
||||||
|
this.updatePerformanceResults();
|
||||||
|
|
||||||
|
if (testResults.aspectRatioUpdated && this.pauseOnArCheck) {
|
||||||
|
(this.aard as any).video.pause();
|
||||||
|
this.aard.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
const resultsDiv = document.getElementById('uw-aard-results');
|
||||||
|
|
||||||
|
const ar = testResults.activeAspectRatio.toFixed(3);
|
||||||
|
|
||||||
|
let out = `
|
||||||
|
LAST STAGE: ${testResults.lastStage} | black level: ${testResults.blackLevel}, threshold: ${testResults.blackThreshold}
|
||||||
|
|
||||||
|
-- ASPECT RATIO
|
||||||
|
Active: ${ar}, changed since last check? ${testResults.aspectRatioUpdated} letterbox width: ${testResults.letterboxWidth} offset ${testResults.letterboxOffset}<br/>
|
||||||
|
<sup>(last: ${this._lastAr})</sup>
|
||||||
|
|
||||||
|
${
|
||||||
|
timers ?
|
||||||
|
`
|
||||||
|
Paused until? ${Date.now() < timers?.pauseUntil ? ((timers?.pauseUntil - Date.now()) / 1000) + 's' : 'not paused'};
|
||||||
|
<sup>now: ${Date.now()}ms; until:${timers?.pauseUntil}ms; diff: ${(+timers?.pauseUntil - Date.now())} </sup>
|
||||||
|
` :
|
||||||
|
`- timers are missing -`
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
image in black level probe (aka "not letterbox"): ${testResults.notLetterbox}
|
||||||
|
`;
|
||||||
|
this._lastAr = ar;
|
||||||
|
|
||||||
|
resultsDiv.innerHTML = out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private setOverlayVisibility() {
|
||||||
|
document.getElementById('uw-aard-debug-ui_body').style.display = this.uiVisibility.detectionDetails ? 'flex' : 'none';
|
||||||
|
document.getElementById('uw-aard-debug_hide-detection-details').style.display = this.uiVisibility.detectionDetails ? '' : 'none';
|
||||||
|
document.getElementById('uw-aard-debug_show-detection-details').style.display = this.uiVisibility.detectionDetails ? 'none' : '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
2138
src/ext/module/aard/AardLegacy.ts
Normal file
2138
src/ext/module/aard/AardLegacy.ts
Normal file
File diff suppressed because it is too large
Load Diff
135
src/ext/module/aard/AardTimers.ts
Normal file
135
src/ext/module/aard/AardTimers.ts
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
export interface AardPerformanceData {
|
||||||
|
start: number;
|
||||||
|
draw: number;
|
||||||
|
getImage: number;
|
||||||
|
|
||||||
|
fastBlackLevel: number;
|
||||||
|
|
||||||
|
// optional ones are only available in legacy aard
|
||||||
|
guardLine?: number; // actually times both guard line and image line checks
|
||||||
|
edgeScan?: number; // includes validation step
|
||||||
|
gradient?: number;
|
||||||
|
scanResults?: number;
|
||||||
|
subtitleScan: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export class AardTimer {
|
||||||
|
private currentIndex: number = -1;
|
||||||
|
|
||||||
|
private aardPerformanceDataBuffer: AardPerformanceData[];
|
||||||
|
|
||||||
|
current: AardPerformanceData;
|
||||||
|
previous: AardPerformanceData;
|
||||||
|
average: AardPerformanceData;
|
||||||
|
lastChange: AardPerformanceData;
|
||||||
|
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
// we need to deep clone, otherwise all buffer objects will point to the same object
|
||||||
|
// (this makes calculating averages impossible)
|
||||||
|
this.aardPerformanceDataBuffer = JSON.parse(JSON.stringify(new Array<AardPerformanceData>(64).fill(this.getEmptyMeasurement())));
|
||||||
|
this.current = this.aardPerformanceDataBuffer[0];
|
||||||
|
this.previous = undefined;
|
||||||
|
this.lastChange = this.getEmptyMeasurement();
|
||||||
|
this.average = this.getEmptyMeasurement();
|
||||||
|
}
|
||||||
|
|
||||||
|
private getEmptyMeasurement(): AardPerformanceData {
|
||||||
|
return {
|
||||||
|
start: -1,
|
||||||
|
draw: -1,
|
||||||
|
getImage: -1,
|
||||||
|
fastBlackLevel: -1,
|
||||||
|
guardLine: -1,
|
||||||
|
edgeScan: -1,
|
||||||
|
gradient: -1,
|
||||||
|
scanResults: -1,
|
||||||
|
subtitleScan: -1,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private clearMeasurement(index: number) {
|
||||||
|
this.aardPerformanceDataBuffer[index].draw = -1;
|
||||||
|
this.aardPerformanceDataBuffer[index].getImage = -1;
|
||||||
|
this.aardPerformanceDataBuffer[index].fastBlackLevel = -1;
|
||||||
|
this.aardPerformanceDataBuffer[index].guardLine = -1;
|
||||||
|
this.aardPerformanceDataBuffer[index].edgeScan = -1;
|
||||||
|
this.aardPerformanceDataBuffer[index].gradient = -1;
|
||||||
|
this.aardPerformanceDataBuffer[index].scanResults = -1;
|
||||||
|
this.aardPerformanceDataBuffer[index].subtitleScan = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
next() {
|
||||||
|
// go to next buffer position;
|
||||||
|
this.currentIndex = (this.currentIndex + 1) % this.aardPerformanceDataBuffer.length;
|
||||||
|
this.previous = this.current;
|
||||||
|
this.clearMeasurement(this.currentIndex);
|
||||||
|
|
||||||
|
// TODO: reset values
|
||||||
|
this.current = this.aardPerformanceDataBuffer[this.currentIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
arChanged() { // copy numbers over
|
||||||
|
this.lastChange.draw = this.current.draw;
|
||||||
|
this.lastChange.getImage = this.current.getImage;
|
||||||
|
this.lastChange.fastBlackLevel = this.current.fastBlackLevel;
|
||||||
|
this.lastChange.guardLine = this.current.guardLine;
|
||||||
|
this.lastChange.edgeScan = this.current.edgeScan;
|
||||||
|
this.lastChange.gradient = this.current.gradient;
|
||||||
|
this.lastChange.scanResults = this.current.scanResults;
|
||||||
|
this.lastChange.subtitleScan = this.current.scanResults;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAverage() {
|
||||||
|
for (let i = 0; i < this.aardPerformanceDataBuffer.length; i++) {
|
||||||
|
const sample = this.aardPerformanceDataBuffer[i];
|
||||||
|
|
||||||
|
if (sample.draw !== -1) {
|
||||||
|
this.average.draw += sample.draw;
|
||||||
|
}
|
||||||
|
if (sample.getImage !== -1) {
|
||||||
|
this.average.getImage += sample.getImage;
|
||||||
|
}
|
||||||
|
if (sample.fastBlackLevel !== -1 && this.average.fastBlackLevel !== null) {
|
||||||
|
this.average.fastBlackLevel += sample.fastBlackLevel;
|
||||||
|
} else {
|
||||||
|
this.average.fastBlackLevel = null;
|
||||||
|
}
|
||||||
|
if (sample.guardLine !== -1 && this.average.guardLine !== null) {
|
||||||
|
this.average.guardLine += sample.guardLine;
|
||||||
|
} else {
|
||||||
|
this.average.guardLine = null;
|
||||||
|
}
|
||||||
|
if (sample.edgeScan !== -1 && this.average.edgeScan !== null) {
|
||||||
|
this.average.edgeScan += sample.edgeScan;
|
||||||
|
} else {
|
||||||
|
this.average.edgeScan = null;
|
||||||
|
}
|
||||||
|
if (sample.gradient !== -1 && this.average.gradient !== null) {
|
||||||
|
this.average.gradient += sample.gradient;
|
||||||
|
} else {
|
||||||
|
this.average.edgeScan = null;
|
||||||
|
}
|
||||||
|
if (sample.scanResults !== -1 && this.average.scanResults !== null) {
|
||||||
|
this.average.scanResults += sample.scanResults;
|
||||||
|
} else {
|
||||||
|
this.average.scanResults = null;
|
||||||
|
}
|
||||||
|
if (sample.subtitleScan !== -1 && this.average.subtitleScan !== null) {
|
||||||
|
this.average.subtitleScan += sample.subtitleScan;
|
||||||
|
} else {
|
||||||
|
this.average.subtitleScan = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.average.draw /= this.aardPerformanceDataBuffer.length;
|
||||||
|
this.average.getImage /= this.aardPerformanceDataBuffer.length;
|
||||||
|
this.average.fastBlackLevel /= this.aardPerformanceDataBuffer.length;
|
||||||
|
this.average.guardLine = this.average.guardLine === null ? -1 : (this.average.guardLine / this.aardPerformanceDataBuffer.length);
|
||||||
|
this.average.edgeScan = this.average.edgeScan === null ? -1 : (this.average.guardLine / this.aardPerformanceDataBuffer.length);
|
||||||
|
this.average.gradient = this.average.gradient === null ? -1 : (this.average.guardLine / this.aardPerformanceDataBuffer.length);
|
||||||
|
this.average.scanResults = this.average.scanResults === null ? -1 : (this.average.guardLine / this.aardPerformanceDataBuffer.length);
|
||||||
|
this.average.subtitleScan = this.average.subtitleScan === null ? -1 : (this.average.subtitleScan /this.aardPerformanceDataBuffer.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
export enum AardUncertainReason {
|
||||||
|
InsufficientEdgeDetectionQuality = 1,
|
||||||
|
LetterboxNotCenteredEnough = 2,
|
||||||
|
TopAndBottomRowMismatch = 3,
|
||||||
|
}
|
||||||
7
src/ext/module/aard/enums/aard-mode.enum.ts
Normal file
7
src/ext/module/aard/enums/aard-mode.enum.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
enum AardMode {
|
||||||
|
Continuous = 0,
|
||||||
|
UntilDetection = 1,
|
||||||
|
Timeout
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AardMode;
|
||||||
19
src/ext/module/aard/enums/aard-performance-data.enum.ts
Normal file
19
src/ext/module/aard/enums/aard-performance-data.enum.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { AardPerformanceMeasurement } from './aard-performance-measurements.enum';
|
||||||
|
|
||||||
|
export interface AardPerformanceData {
|
||||||
|
total: AardPerformanceMeasurement,
|
||||||
|
theoretical: AardPerformanceMeasurement,
|
||||||
|
imageDraw: AardPerformanceMeasurement
|
||||||
|
blackFrameDraw: AardPerformanceMeasurement,
|
||||||
|
blackFrame: AardPerformanceMeasurement,
|
||||||
|
fastLetterbox: AardPerformanceMeasurement,
|
||||||
|
edgeDetect: AardPerformanceMeasurement,
|
||||||
|
|
||||||
|
imageDrawCount: number,
|
||||||
|
blackFrameDrawCount: number,
|
||||||
|
blackFrameCount: number,
|
||||||
|
fastLetterboxCount: number,
|
||||||
|
edgeDetectCount: number,
|
||||||
|
|
||||||
|
aardActive: boolean, // whether autodetection is currently running or not
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user