Compare commits

...

68 Commits

Author SHA1 Message Date
a5fdef0196 version bump for release 2025-06-18 02:19:18 +02:00
f10eee335d version bump for testing version 2025-06-18 01:41:02 +02:00
20856891b6 Don't open global settings window in iframe 2025-06-18 01:15:45 +02:00
60dd1193ab Detect embedded content 2025-06-18 01:04:42 +02:00
2bc95af73c Fix iframe detection 2025-06-17 22:49:16 +02:00
178bb65b4a Improve embedded frame detection 2025-06-17 20:12:44 +02:00
f7f046ea53 Propagate messages across server boundary 2025-06-16 21:58:35 +02:00
ec0896e17a Implement zoom in extension popup, in-player UI 2025-05-19 01:18:48 +02:00
a5f35248bd Event-bus: Auto-forward across iframe and commsServer 2025-05-19 01:17:09 +02:00
a05eccce9e fix zoom config 2025-05-19 01:15:05 +02:00
e118777636 Fix debounce 2025-05-19 01:14:19 +02:00
7998e8158e Fix log coloration 2025-05-07 00:45:08 +02:00
14f837d1f0 EventBus: prevent backflow of messages that originate from CommsServer back into CommsServer 2025-05-07 00:42:25 +02:00
cff1d3cf16 Cache active tab in comms server
Previously, if console window was active, this.activeTab would return nothing, and the popup would break. If that happens now, popup will pretend it's on the last active site.
2025-05-07 00:41:45 +02:00
b2cd96982e Further logging fixes 2025-05-04 21:00:15 +02:00
a89eb8e857 Whoops, missed some bits in the cleanup 2025-05-04 02:37:45 +02:00
7c1ccd2cc9 Add contributing.md and license.md 2025-05-04 02:30:56 +02:00
e3a12bc601 Cleanup of unused imports 2025-05-04 02:29:33 +02:00
e20508956a Replace old logger with new logger 2025-05-04 02:18:58 +02:00
4000d0e027 Start rewriting logs 2025-05-01 00:55:22 +02:00
77fcced776 Fix saving settings for sites that inherit configurations from a different site 2025-04-28 01:38:13 +02:00
e67b5a227f Allow some cross-domain settings sharing, handle settings inheritance, don't save auto-detected player index 2025-04-28 01:23:12 +02:00
150a0a8a90 Process iframe data in the popup, remove vestigal code 2025-04-27 19:51:21 +02:00
269dddc92e Fix legacy canvas loading 2025-04-27 17:53:50 +02:00
508ef5cbbb Fix calculation of averages in AardTimers 2025-04-27 17:52:04 +02:00
500d06204c Move default settings to 'other sites' tab 2025-04-27 01:12:06 +02:00
fe8436179f Remove orphan components, update dictionary 2025-04-26 23:28:17 +02:00
adedfec1d0 Update popup appearance 2025-04-26 23:27:09 +02:00
b974111eb4 Fix zoom 2025-04-26 23:24:57 +02:00
3d7b50a2e3 Get zoom options to mostly work 2025-04-26 04:23:57 +02:00
32c11d1f61 Fix snapshots 2025-04-26 01:11:51 +02:00
5c48747d0f Add new zoom options (but they don't do anything yet) 2025-04-26 01:11:31 +02:00
5a0abb3595 Fix settings migrations 2025-04-26 01:09:49 +02:00
95b98c9053 Fix loading snapshots 2025-04-26 01:08:43 +02:00
518e09e3cb Changelog 2025-04-22 02:53:05 +02:00
ad632b0680 Add snapshots to settings page 2025-04-22 02:52:41 +02:00
73aa925067 Don't do AR changes on negative aspect ratios 2025-04-22 02:44:28 +02:00
391b0ac7ab Settings snapshots 2025-04-22 02:43:44 +02:00
ae2bb7afcd Fix UI hiding 2025-04-22 02:37:36 +02:00
0681c3ae08 UI improvements 2025-04-22 02:36:34 +02:00
4aa96bb288 Changes to debugging UI 2025-04-21 22:24:32 +02:00
b24739d70d Put "pls report bugs" in 'what's new' panel as well 2025-04-20 19:50:51 +02:00
dffe8e055a put dev overlays into settings 2025-04-20 19:29:47 +02:00
adadc8bb3e version bump 2025-04-20 19:28:59 +02:00
5f3562fe5b Remove orphan settings 2025-04-20 16:11:57 +02:00
0d7c535a70 revive zoom 2025-04-20 16:11:17 +02:00
39f39f23a7 Prevent more than one concurrent trackDimensionChanges from running 2025-04-15 19:23:42 +02:00
ff0de6f2e7 Version bump 2025-04-15 19:05:48 +02:00
8658b3c0d3 Changelog 2025-04-15 19:03:29 +02:00
8551bc2dc1 Add performance measurements to Aard 2025-04-15 18:51:34 +02:00
a0abf336cf fix #269 2025-04-10 00:46:44 +02:00
7d71dd0507 Allow debug canvas to be shown or hidden from settings 2025-04-10 00:46:22 +02:00
54c190f86c Updated toys 2025-04-05 01:31:06 +02:00
e198c69741 Added toys that enable visual debugging of Aard 2025-04-03 02:59:25 +02:00
7c1d3e52e5 Instead of elements closer to the <video> tag, prefer best-matching element closer to page root.
This ensures UI always renders on top of player if enabled.
2025-03-31 23:26:03 +02:00
5460719297 css fixes 2025-03-31 00:28:35 +02:00
83b770e4e8 Start updating changelog 2025-03-31 00:26:14 +02:00
a5c9835d9d Fix problem where 'default' is always 'disabled' 2025-03-31 00:20:43 +02:00
f0840b78e1 Change handling of getPlayer() 2025-03-30 23:49:13 +02:00
ab79f1e0e6 TODO: some day finish rewriting UI in a way that loads iframe only when expressly requested. 2025-03-30 23:48:24 +02:00
16a2f511bb Replaced v-model in <select> with :value
Did this really fix #270?
2025-03-30 01:27:28 +01:00
39fdd72e9f Don't propagate bad site data from uwserver to extension popup 2025-03-30 01:24:37 +01:00
f93a7e284c Remove InPlayerUIAdvertisement component 2025-03-30 01:24:08 +01:00
b5e480a486 'dev mode' is now saved under settings. 'debug' panel is only shown if dev mode is active 2025-03-30 00:54:47 +01:00
bf128babcc Started logger rewrite. Gave up. 2025-03-30 00:27:50 +01:00
46d1ad221f Re-add debugger panel, strip down json editor 2025-03-30 00:04:30 +01:00
7aacbec108 Reset settings editor on settings import and reset 2025-03-29 17:17:20 +01:00
f77506c454 Settings export, settings import, json editor for settings 2025-03-29 17:11:04 +01:00
90 changed files with 5343 additions and 2749 deletions

View File

@ -13,6 +13,7 @@
"com",
"comms",
"csui",
"cycleable",
"decycle",
"dinked",
"dinks",
@ -46,6 +47,7 @@
"reddit",
"rescan",
"resizer",
"resizers",
"scrollbar",
"shitiness",
"smallcaps",

View File

@ -2,6 +2,25 @@
## v6.0 (current major)
### 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 detects embedded content (but not always)
* Added `www.youtube-nocookie.com` to "officially supported" list
### v6.2.5
* Popup appearance changed — UI advertisement panel was moved to the popup header
* 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.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

39
CONTRIBUTING.md Normal file
View 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.

35
LICENSE.MD Normal file
View 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.

411
package-lock.json generated
View File

@ -1,6 +1,6 @@
{
"name": "ultrawidify",
"version": "6.2.4",
"version": "6.3.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -28,7 +28,6 @@
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
"integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
"dev": true,
"requires": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.24"
@ -1376,6 +1375,109 @@
"to-fast-properties": "^2.0.0"
}
},
"@codemirror/autocomplete": {
"version": "6.18.6",
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.6.tgz",
"integrity": "sha512-PHHBXFomUs5DF+9tCOM/UoW6XQ4R44lLNNhRaW9PKPTU0D7lIjRg3ElxaJnTwsl/oHiR93WSXDBrekhoUGCPtg==",
"requires": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.17.0",
"@lezer/common": "^1.0.0"
}
},
"@codemirror/commands": {
"version": "6.8.0",
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.8.0.tgz",
"integrity": "sha512-q8VPEFaEP4ikSlt6ZxjB3zW72+7osfAYW9i8Zu943uqbKuz6utc1+F170hyLUCUltXORjQXRyYQNfkckzA/bPQ==",
"requires": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.4.0",
"@codemirror/view": "^6.27.0",
"@lezer/common": "^1.1.0"
}
},
"@codemirror/lang-json": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.1.tgz",
"integrity": "sha512-+T1flHdgpqDDlJZ2Lkil/rLiRy684WMLc74xUnjJH48GQdfJo/pudlTRreZmKwzP8/tGdKf83wlbAdOCzlJOGQ==",
"requires": {
"@codemirror/language": "^6.0.0",
"@lezer/json": "^1.0.0"
}
},
"@codemirror/language": {
"version": "6.11.0",
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.0.tgz",
"integrity": "sha512-A7+f++LodNNc1wGgoRDTt78cOwWm9KVezApgjOMp1W4hM0898nsqBXwF+sbePE7ZRcjN7Sa1Z5m2oN27XkmEjQ==",
"requires": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.23.0",
"@lezer/common": "^1.1.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0",
"style-mod": "^4.0.0"
}
},
"@codemirror/lint": {
"version": "6.8.5",
"resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.5.tgz",
"integrity": "sha512-s3n3KisH7dx3vsoeGMxsbRAgKe4O1vbrnKBClm99PU0fWxmxsx5rR2PfqQgIt+2MMJBHbiJ5rfIdLYfB9NNvsA==",
"requires": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.35.0",
"crelt": "^1.0.5"
}
},
"@codemirror/search": {
"version": "6.5.10",
"resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.10.tgz",
"integrity": "sha512-RMdPdmsrUf53pb2VwflKGHEe1XVM07hI7vV2ntgw1dmqhimpatSJKva4VA9h4TLUDOD4EIF02201oZurpnEFsg==",
"requires": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"crelt": "^1.0.5"
}
},
"@codemirror/state": {
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz",
"integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==",
"requires": {
"@marijn/find-cluster-break": "^1.0.0"
}
},
"@codemirror/view": {
"version": "6.36.5",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.36.5.tgz",
"integrity": "sha512-cd+FZEUlu3GQCYnguYm3EkhJ8KJVisqqUsCOKedBoAt/d9c76JUUap6U0UrpElln5k6VyrEOYliMuDAKIeDQLg==",
"requires": {
"@codemirror/state": "^6.5.0",
"style-mod": "^4.1.0",
"w3c-keyname": "^2.2.4"
}
},
"@fortawesome/fontawesome-common-types": {
"version": "6.7.2",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.7.2.tgz",
"integrity": "sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg=="
},
"@fortawesome/free-regular-svg-icons": {
"version": "6.7.2",
"resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.7.2.tgz",
"integrity": "sha512-7Z/ur0gvCMW8G93dXIQOkQqHo2M5HLhYrRVC0//fakJXxcF1VmMPsxnG6Ee8qEylA8b8Q3peQXWMNZ62lYF28g==",
"requires": {
"@fortawesome/fontawesome-common-types": "6.7.2"
}
},
"@fortawesome/free-solid-svg-icons": {
"version": "6.7.2",
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.7.2.tgz",
"integrity": "sha512-GsBrnOzU8uj0LECDfD5zomZJIjrPhIlWU82AHwa2s40FKH+kcxQaBvBo3Z4TxyZHIyX8XTDxsyA33/Vx9eFuQA==",
"requires": {
"@fortawesome/fontawesome-common-types": "6.7.2"
}
},
"@hapi/address": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@hapi/address/-/address-2.1.4.tgz",
@ -1455,6 +1557,57 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"@jsep-plugin/assignment": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@jsep-plugin/assignment/-/assignment-1.3.0.tgz",
"integrity": "sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ=="
},
"@jsep-plugin/regex": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@jsep-plugin/regex/-/regex-1.0.4.tgz",
"integrity": "sha512-q7qL4Mgjs1vByCaTnDFcBnV9HS7GVPJX5vyVoCgZHNSC9rjwIlmbXG5sUuorR5ndfHAIlJ8pVStxvjXHbNvtUg=="
},
"@jsonquerylang/jsonquery": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/@jsonquerylang/jsonquery/-/jsonquery-4.1.1.tgz",
"integrity": "sha512-Rfyvq70Zrb561BqSuXLsl0rG0/1tz913EQDL/4zpkp+laFGUxXIVPSaJWcdREJwADXLZDkQyaWplzEaPQvh+7A=="
},
"@lezer/common": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.3.tgz",
"integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA=="
},
"@lezer/highlight": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz",
"integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==",
"requires": {
"@lezer/common": "^1.0.0"
}
},
"@lezer/json": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.3.tgz",
"integrity": "sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==",
"requires": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0"
}
},
"@lezer/lr": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz",
"integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==",
"requires": {
"@lezer/common": "^1.0.0"
}
},
"@marijn/find-cluster-break": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz",
"integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g=="
},
"@mdi/font": {
"version": "6.9.96",
"resolved": "https://registry.npmjs.org/@mdi/font/-/font-6.9.96.tgz",
@ -1582,12 +1735,27 @@
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
"dev": true
},
"@replit/codemirror-indentation-markers": {
"version": "6.5.3",
"resolved": "https://registry.npmjs.org/@replit/codemirror-indentation-markers/-/codemirror-indentation-markers-6.5.3.tgz",
"integrity": "sha512-hL5Sfvw3C1vgg7GolLe/uxX5T3tmgOA3ZzqlMv47zjU1ON51pzNWiVbS22oh6crYhtVhv8b3gdXwoYp++2ilHw=="
},
"@sindresorhus/is": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.7.0.tgz",
"integrity": "sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow==",
"dev": true
},
"@sphinxxxx/color-conversion": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/@sphinxxxx/color-conversion/-/color-conversion-2.2.2.tgz",
"integrity": "sha512-XExJS3cLqgrmNBIP3bBw6+1oQ1ksGjFh0+oClDKFYpCCqx/hlqwWO5KO/S63fzUo67SxI9dMrF0y5T/Ey7h8Zw=="
},
"@sveltejs/acorn-typescript": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.5.tgz",
"integrity": "sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ=="
},
"@types/accepts": {
"version": "1.3.7",
"resolved": "https://registry.npmjs.org/@types/accepts/-/accepts-1.3.7.tgz",
@ -1671,6 +1839,11 @@
"es6-promise": "*"
}
},
"@types/estree": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
"integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="
},
"@types/express": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz",
@ -3032,6 +3205,11 @@
"sprintf-js": "~1.0.2"
}
},
"aria-query": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
"integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="
},
"arr-diff": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz",
@ -3375,6 +3553,11 @@
"integrity": "sha512-3AungXC4I8kKsS9PuS4JH2nc+0bVY/mjgrephHTIi8fpEeGsTHBUJeosp0Wc1myYMElmD0B3Oc4XL/HVJ4PV2g==",
"dev": true
},
"axobject-query": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
"integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="
},
"babel-code-frame": {
"version": "6.26.0",
"resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz",
@ -4760,6 +4943,11 @@
"mimic-response": "^1.0.0"
}
},
"clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="
},
"cmd-shim": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/cmd-shim/-/cmd-shim-3.0.3.tgz",
@ -4785,6 +4973,11 @@
"integrity": "sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==",
"dev": true
},
"codemirror-wrapped-line-indent": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/codemirror-wrapped-line-indent/-/codemirror-wrapped-line-indent-1.0.8.tgz",
"integrity": "sha512-5UwuHCz4oAZuvot1DbfFxSxJacTESdNGa/KpJD7HfpVpDAJdgB1vV9OG4b4pkJqPWuOfIpFLTQEKS85kTpV+XA=="
},
"collection-visit": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz",
@ -5366,6 +5559,11 @@
"sha.js": "^2.4.8"
}
},
"crelt": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g=="
},
"cross-env": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-5.2.1.tgz",
@ -6015,6 +6213,11 @@
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
"dev": true
},
"diff-sequences": {
"version": "29.6.3",
"resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz",
"integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q=="
},
"diffie-hellman": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz",
@ -6366,12 +6569,25 @@
"estraverse": "^4.1.1"
}
},
"esm-env": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz",
"integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="
},
"esprima": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
"dev": true
},
"esrap": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/esrap/-/esrap-1.4.5.tgz",
"integrity": "sha512-CjNMjkBWWZeHn+VX+gS8YvFwJ5+NDhg8aWZBSFJPR8qQduDNjbJodA2WcwCm7uQa5Rjqj+nZvVmceg1RbHFB9g==",
"requires": {
"@jridgewell/sourcemap-codec": "^1.4.15"
}
},
"esrecurse": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
@ -6731,8 +6947,7 @@
"fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
},
"fast-glob": {
"version": "2.2.7",
@ -6754,6 +6969,11 @@
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
"dev": true
},
"fast-uri": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz",
"integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw=="
},
"fastparse": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.2.tgz",
@ -8115,6 +8335,11 @@
"integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==",
"dev": true
},
"immutable-json-patch": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/immutable-json-patch/-/immutable-json-patch-6.0.1.tgz",
"integrity": "sha512-BHL/cXMjwFZlTOffiWNdY8ZTvNyYLrutCnWxrcKPHr5FqpAb6vsO6WWSPnVSys3+DruFN6lhHJJPHi8uELQL5g=="
},
"import-fresh": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@ -8618,6 +8843,14 @@
"integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==",
"dev": true
},
"is-reference": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
"integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==",
"requires": {
"@types/estree": "^1.0.6"
}
},
"is-regex": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz",
@ -8770,6 +9003,11 @@
"integrity": "sha512-fnjC0up+0SjEJtgmmG+teeel68kutkvzfctO/KxE3qJlbunkJYAshgH3boU++gSBHP8z5/r0ts0qRIrHf0RTQQ==",
"dev": true
},
"jmespath": {
"version": "0.16.0",
"resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz",
"integrity": "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw=="
},
"js-base64": {
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.6.4.tgz",
@ -8865,6 +9103,11 @@
}
}
},
"jsep": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/jsep/-/jsep-1.4.0.tgz",
"integrity": "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw=="
},
"jsesc": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
@ -8904,6 +9147,11 @@
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"dev": true
},
"json-source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/json-source-map/-/json-source-map-0.6.1.tgz",
"integrity": "sha512-1QoztHPsMQqhDq0hlXY5ZqcEdUzxQEIxgFkKl4WUp2pgShObl+9ovi4kRh2TfvAfxAoHOJ9vIMEqk3k4iex7tg=="
},
"json-stringify-safe": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
@ -8926,6 +9174,21 @@
"graceful-fs": "^4.1.6"
}
},
"jsonpath-plus": {
"version": "10.3.0",
"resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-10.3.0.tgz",
"integrity": "sha512-8TNmfeTCk2Le33A3vRRwtuworG/L5RrgMvdjhKZxvyShO+mBu2fP50OWUjRLNtvw344DdDarFh9buFAZs5ujeA==",
"requires": {
"@jsep-plugin/assignment": "^1.3.0",
"@jsep-plugin/regex": "^1.0.4",
"jsep": "^1.4.0"
}
},
"jsonrepair": {
"version": "3.12.0",
"resolved": "https://registry.npmjs.org/jsonrepair/-/jsonrepair-3.12.0.tgz",
"integrity": "sha512-SWfjz8SuQ0wZjwsxtSJ3Zy8vvLg6aO/kxcp9TWNPGwJKgTZVfhNEQBMk/vPOpYCDFWRxD6QWuI6IHR1t615f0w=="
},
"jsprim": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz",
@ -9030,6 +9293,11 @@
"json5": "^1.0.1"
}
},
"locate-character": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz",
"integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="
},
"locate-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
@ -9044,6 +9312,11 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"lodash-es": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="
},
"lodash.camelcase": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
@ -9263,6 +9536,11 @@
"fs-monkey": "^1.0.4"
}
},
"memoize-one": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
"integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw=="
},
"memory-fs": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.5.0.tgz",
@ -9563,6 +9841,11 @@
"to-regex": "^3.0.1"
}
},
"natural-compare-lite": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz",
"integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g=="
},
"ndjson": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/ndjson/-/ndjson-1.5.0.tgz",
@ -13332,6 +13615,11 @@
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="
},
"require-from-string": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="
},
"require-main-filename": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
@ -14398,6 +14686,11 @@
"escape-string-regexp": "^1.0.2"
}
},
"style-mod": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz",
"integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw=="
},
"subscriptions-transport-ws": {
"version": "0.9.19",
"resolved": "https://registry.npmjs.org/subscriptions-transport-ws/-/subscriptions-transport-ws-0.9.19.tgz",
@ -14424,6 +14717,47 @@
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="
},
"svelte": {
"version": "5.25.3",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.25.3.tgz",
"integrity": "sha512-J9rcZ/xVJonAoESqVGHHZhrNdVbrCfkdB41BP6eiwHMoFShD9it3yZXApVYMHdGfCshBsZCKsajwJeBbS/M1zg==",
"requires": {
"@ampproject/remapping": "^2.3.0",
"@jridgewell/sourcemap-codec": "^1.5.0",
"@sveltejs/acorn-typescript": "^1.0.5",
"@types/estree": "^1.0.5",
"acorn": "^8.12.1",
"aria-query": "^5.3.1",
"axobject-query": "^4.1.0",
"clsx": "^2.1.1",
"esm-env": "^1.2.1",
"esrap": "^1.4.3",
"is-reference": "^3.0.3",
"locate-character": "^3.0.0",
"magic-string": "^0.30.11",
"zimmerframe": "^1.1.2"
},
"dependencies": {
"@jridgewell/sourcemap-codec": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="
},
"acorn": {
"version": "8.14.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg=="
},
"magic-string": {
"version": "0.30.17",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
"integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
"requires": {
"@jridgewell/sourcemap-codec": "^1.5.0"
}
}
}
},
"svgo": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/svgo/-/svgo-0.7.2.tgz",
@ -15475,6 +15809,65 @@
"builtins": "^1.0.3"
}
},
"vanilla-jsoneditor": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/vanilla-jsoneditor/-/vanilla-jsoneditor-3.3.0.tgz",
"integrity": "sha512-HvCh0qaeQwB4HUW+G7SAKpMPy7jugnt697W9b4zJ4SkbKkjvX3Z1HEBuImtllT47IshR3HcPg8rHHA0Uq/e8Eg==",
"requires": {
"@codemirror/autocomplete": "^6.18.1",
"@codemirror/commands": "^6.7.1",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/language": "^6.10.3",
"@codemirror/lint": "^6.8.2",
"@codemirror/search": "^6.5.6",
"@codemirror/state": "^6.4.1",
"@codemirror/view": "^6.34.1",
"@fortawesome/free-regular-svg-icons": "^6.6.0",
"@fortawesome/free-solid-svg-icons": "^6.6.0",
"@jsonquerylang/jsonquery": "^3.1.1 || ^4.0.0",
"@lezer/highlight": "^1.2.1",
"@replit/codemirror-indentation-markers": "^6.5.3",
"ajv": "^8.17.1",
"codemirror-wrapped-line-indent": "^1.0.8",
"diff-sequences": "^29.6.3",
"immutable-json-patch": "^6.0.1",
"jmespath": "^0.16.0",
"json-source-map": "^0.6.1",
"jsonpath-plus": "^10.3.0",
"jsonrepair": "^3.0.0",
"lodash-es": "^4.17.21",
"memoize-one": "^6.0.0",
"natural-compare-lite": "^1.4.0",
"svelte": "^5.0.0",
"vanilla-picker": "^2.12.3"
},
"dependencies": {
"ajv": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"requires": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2"
}
},
"json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
}
}
},
"vanilla-picker": {
"version": "2.12.3",
"resolved": "https://registry.npmjs.org/vanilla-picker/-/vanilla-picker-2.12.3.tgz",
"integrity": "sha512-qVkT1E7yMbUsB2mmJNFmaXMWE2hF8ffqzMMwe9zdAikd8u2VfnsVY2HQcOUi2F38bgbxzlJBEdS1UUhOXdF9GQ==",
"requires": {
"@sphinxxxx/color-conversion": "^2.2.2"
}
},
"vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
@ -15951,6 +16344,11 @@
"resolved": "https://registry.npmjs.org/vuex-webextensions/-/vuex-webextensions-1.3.3.tgz",
"integrity": "sha512-Qz+KmF4CYLfomAIuEjY4A9DYqzwXZo18TkvTyO84DkW/s274iI2IyuOSFZoLcVHMS5BjUhRbyFclqqAwz58zgw=="
},
"w3c-keyname": {
"version": "2.2.8",
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="
},
"watch": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/watch/-/watch-1.0.2.tgz",
@ -16588,6 +16986,11 @@
"zen-observable": "^0.8.0"
}
},
"zimmerframe": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.2.tgz",
"integrity": "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w=="
},
"zip-stream": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-2.1.3.tgz",

View File

@ -1,6 +1,6 @@
{
"name": "ultrawidify",
"version": "6.2.4",
"version": "6.3.0",
"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>",
"scripts": {
@ -36,6 +36,7 @@
"lodash": "^4.17.21",
"mdi-vue": "^3.0.11",
"typescript": "^4.4.4",
"vanilla-jsoneditor": "^3.3.0",
"vue": "^3.2.21",
"vue-style-loader": "^4.1.3",
"vuex": "^4.0.2",

View File

@ -1,6 +1,13 @@
import AspectRatioType from '../enums/AspectRatioType.enum';
export enum ArVariant {
Crop = undefined,
Zoom = 1
}
export interface Ar {
type: AspectRatioType,
ratio?: number
ratio?: number,
variant?: ArVariant,
offset?: number,
}

View File

@ -12,6 +12,13 @@ export enum ExtensionEnvironment {
Fullscreen = 'fullscreen',
}
export interface DevUiConfig {
aardDebugOverlay: {
showOnStartup: boolean,
showDetectionDetails: boolean,
}
}
export interface KeyboardShortcutInterface {
key?: string,
code?: string,
@ -161,11 +168,16 @@ export interface AardSettings {
}
}
interface DevSettings {
loadFromSnapshot: boolean,
}
interface SettingsInterface {
_updateFlags?: {
requireReload?: SettingsReloadFlags,
forSite?: string
}
dev: DevSettings,
arDetect: AardSettings,
@ -183,7 +195,9 @@ interface SettingsInterface {
offsetX: number, // fed to translateX(offsetX + '%'). Valid range [-100, 0]
offsetY: number // fed to translateY(offsetY + '%'). Valid range [-100, 100]
},
}
},
devMode?: boolean,
dev: DevUiConfig,
}
restrictions?: RestrictionsSettings;
@ -231,6 +245,7 @@ interface SettingsInterface {
pan?: any,
version?: string,
preventReload?: boolean,
lastModified?: Date,
// -----------------------------------------
// ::: MITIGATIONS :::
@ -243,33 +258,6 @@ interface SettingsInterface {
limit?: number,
}
}
// -----------------------------------------
// ::: ACTIONS :::
// -----------------------------------------
// Nastavitve za ukaze. Zamenja stare nastavitve za bližnične tipke.
//
// Polje 'shortcut' je tabela, če se slučajno lotimo kdaj delati choordov.
actions: {
name?: string, // name displayed in settings
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,
}[],
// This object fulfills the same purpose as 'actions', but is written in less retarded and overly
// complicated way. Hopefully it'll be easier to maintain it that way.
commands?: {

View File

@ -1,5 +1,5 @@
<template>
<div class="uw-clickthrough relative w-100 h-100">
<div class="uw-clickthrough relative w-full h-full">
<template v-for="rectangle of drawnRectangles" :key="rectangle.id ?? rectangle">
<!-- Player element overlays -->
@ -44,13 +44,6 @@ export default {
},
async created() {
this.logger = new Logger();
// this prolly needs to be taken out
await this.logger.init({
allowLogging: true,
});
/**
* Setup the "companion" onMouseMove handler to the one in the content script.
* We can handle events with the same function we use to handle events from

View File

@ -64,7 +64,7 @@
</GhettoContextMenuOption>
</slot>
</GhettoContextMenu>
<!-- <GhettoContextMenu alignment="right">
<GhettoContextMenu alignment="right">
<template v-slot:activator>
Zoom
</template>
@ -86,7 +86,7 @@
/>
</GhettoContextMenuItem>
</slot>
</GhettoContextMenu> -->
</GhettoContextMenu>
<GhettoContextMenu alignment="right">
<template v-slot:activator>
<div class="context-item">
@ -144,11 +144,13 @@
Site compatibility:
<SupportLevelIndicator
:siteSupportLevel="siteSupportLevel"
supportLevelStyle="font-size: 0.69rem !important;"
tooltipStyle="font-size: 0.8rem;"
>
</SupportLevelIndicator>
<div v-if="statusFlags.hasDrm" class="aard-blocked">
Autodetection potentially<br/>
unavailable due to <a style="color: #fff" href="https://en.wikipedia.org/wiki/Digital_rights_management" target="_blank">DRM</a>.
Autodetection blocked<br/>
by <a style="color: #fff" href="https://en.wikipedia.org/wiki/Digital_rights_management" target="_blank">DRM</a>.
</div>
<div v-else-if="statusFlags.aardErrors?.cors" class="aard-blocked">
Autodetection blocked<br/>
@ -156,7 +158,7 @@
</div>
<div v-else-if="statusFlags.aardErrors?.webglError" class="aard-blocked">
Autodetection unavailable<br/>
due to webgl error.
(webgl error)
</div>
</GhettoContextMenuItem>
</div>
@ -203,7 +205,6 @@ import GhettoContextMenuItem from '@csui/src/components/GhettoContextMenuItem.vu
import GhettoContextMenuOption from '@csui/src/components/GhettoContextMenuOption.vue';
import AlignmentOptionsControlComponent from '@csui/src/PlayerUiPanels/AlignmentOptionsControlComponent.vue';
import BrowserDetect from '@src/ext/conf/BrowserDetect';
import Logger from '@src/ext/lib/Logger';
import Settings from '@src/ext/lib/Settings';
import EventBus from '@src/ext/lib/EventBus';
import UIProbeMixin from '@csui/src/utils/UIProbeMixin';
@ -212,6 +213,8 @@ import CommsMixin from '@csui/src/utils/CommsMixin';
import SupportLevelIndicator from '@csui/src/components/SupportLevelIndicator.vue';
import TriggerZoneEditor from '@csui/src/components/TriggerZoneEditor.vue';
import ZoomControl from '@csui/src/popup/player-menu/ZoomControl.vue';
import { LogAggregator } from '@src/ext/lib/logging/LogAggregator';
import { ComponentLogger } from '@src/ext/lib/logging/ComponentLogger';
export default {
components: {
@ -253,6 +256,7 @@ export default {
BrowserDetect: BrowserDetect,
settingsInitialized: false,
eventBus: new EventBus(),
logAggregator: null,
logger: null,
// NOTE: chromium doesn't allow us to access window.parent.location
@ -324,12 +328,8 @@ export default {
}
},
async created() {
this.logger = new Logger();
// this prolly needs to be taken out
await this.logger.init({
allowLogging: true,
});
this.logAggregator = new LogAggregator('player-overlay');
this.logger = new ComponentLogger(this.logAggregator, 'PlayerOverlay.vue');
this.settings = new Settings({afterSettingsSaved: this.updateConfig, logger: this.logger});
this.settings.listenAfterChange(() => this.updateTriggerZones());
@ -483,7 +483,8 @@ export default {
},
acknowledgeNewFeature(featureKey) {
delete this.settings.active.newFeatureTracker[featureKey];
this.settings.active.newFeatureTracker[featureKey].show = 0;
this.settings.active.newFeatureTracker[featureKey].acknowledged = true;
this.settings.saveWithoutReload();
},
newFeatureViewUpdate(featureKey) {
@ -571,7 +572,17 @@ export default {
},
handleBusTunnelIn(payload) {
this.eventBus.send(payload.action, payload.config, payload.routingData);
this.eventBus.send(
payload.action,
payload.config,
{
...payload.context,
borderCrossings: {
...payload.context?.borderCrossings,
iframe: true
}
}
);
},
updateConfig() {
@ -721,6 +732,7 @@ export default {
}
.aard-blocked {
font-size: 0.8rem;
color: #fa6;
}

View File

@ -1,5 +1,5 @@
<template>
<div class="popup-panel">
<div class="popup-panel" style="height: 100vh">
<!--
NOTE the code that makes ultrawidify popup work in firefox regardless of whether the
extension is being displayed in a normal or a small/overflow popup breaks the popup
@ -10,6 +10,7 @@
further than that.
-->
<div v-if="settingsInitialized"
style="height: 100vh"
class="popup flex flex-col no-overflow"
:class="{'popup-chrome': ! BrowserDetect?.firefox}"
>
@ -78,32 +79,22 @@
</div>
<!-- CONTENT -->
<div class="scrollable" style="flex: 7 7; padding: 1rem;">
<div class="scrollable window-content" style="flex: 7 7; padding: 1rem;">
<template v-if="settings && siteSettings">
<InPlayerUIAdvertisement
v-if="selectedTab === 'playerUiCtl'"
:eventBus="eventBus"
/>
<PopupVideoSettings
v-if="selectedTab === 'videoSettings'"
:settings="settings"
:eventBus="eventBus"
:siteSettings="siteSettings"
:hosts="activeHosts"
></PopupVideoSettings>
<!-- <PlayerDetectionPanel
v-if="selectedTab === 'playerDetection'"
:settings="settings"
:eventBus="eventBus"
:siteSettings="siteSettings"
:site="site.host"
>
</PlayerDetectionPanel> -->
<BaseExtensionSettings
v-if="selectedTab === 'extensionSettings'"
:settings="settings"
:eventBus="eventBus"
:siteSettings="siteSettings"
:site="site.host"
:hosts="activeHosts"
>
</BaseExtensionSettings>
<ChangelogPanel
@ -128,17 +119,16 @@ import BaseExtensionSettings from './src/PlayerUiPanels/BaseExtensionSettings.vu
import PlayerDetectionPanel from './src/PlayerUiPanels/PlayerDetectionPanel.vue'
import ChangelogPanel from './src/PlayerUiPanels/ChangelogPanel.vue'
import PopupVideoSettings from './src/popup/panels/PopupVideoSettings.vue'
import InPlayerUIAdvertisement from './src/PlayerUiPanels/InPlayerUiAdvertisement.vue';
import AboutPanel from '@csui/src/popup/panels/AboutPanel.vue'
import Debug from '../ext/conf/Debug';
import BrowserDetect from '../ext/conf/BrowserDetect';
import Comms from '../ext/lib/comms/Comms';
import CommsClient, {CommsOrigin} from '../ext/lib/comms/CommsClient';
import Settings from '../ext/lib/Settings';
import Logger from '../ext/lib/Logger';
import EventBus from '../ext/lib/EventBus';
import {ChromeShittinessMitigations as CSM} from '../common/js/ChromeShittinessMitigations';
import SupportLevelIndicator from '@csui/src/components/SupportLevelIndicator.vue'
import { LogAggregator } from '@src/ext/lib/logging/LogAggregator';
import { ComponentLogger } from '@src/ext/lib/logging/ComponentLogger';
export default {
components: {
@ -147,7 +137,6 @@ export default {
PopupVideoSettings,
PlayerDetectionPanel,
BaseExtensionSettings,
InPlayerUIAdvertisement,
SupportLevelIndicator,
ChangelogPanel,
AboutPanel
@ -160,6 +149,7 @@ export default {
settingsInitialized: false,
narrowPopup: null,
sideMenuVisible: null,
logAggregator: undefined,
logger: undefined,
site: undefined,
siteSettings: undefined,
@ -181,15 +171,15 @@ export default {
}
},
mounted() {
this.tabs.find(x => x.id === 'changelog').highlight = !this.settings.active.whatsNewChecked;
this.tabs.find(x => x.id === 'changelog').highlight = !this.settings.active?.whatsNewChecked;
this.requestSite();
},
async created() {
this.logger = new Logger();
await this.logger.init({
allowLogging: true,
});
try {
this.logAggregator = new LogAggregator('🔵ext-popup🔵');
this.logger = new ComponentLogger(this.logAggregator, 'Popup');
this.settings = new Settings({afterSettingsSaved: () => this.updateConfig(), logger: this.logger});
this.settings = new Settings({afterSettingsSaved: () => this.updateConfig(), logAggregator: this.logAggregator});
await this.settings.init();
this.settingsInitialized = true;
@ -203,6 +193,8 @@ export default {
{
source: this,
function: (config, context) => {
console.log('set-current-site | this.site:', this.site, 'config.site:', config.site);
if (this.site) {
if (!this.site.host) {
// dunno why this fix is needed, but sometimes it is
@ -212,7 +204,6 @@ export default {
this.site = config.site;
// this.selectedSite = this.selectedSite || config.site.host;
this.siteSettings = this.settings.getSiteSettings(this.site.host);
this.eventBus.setupPopupTunnelWorkaround({
origin: CommsOrigin.Popup,
comms: {
@ -220,11 +211,20 @@ export default {
}
});
this.loadFrames(this.site);
this.loadHostnames();
this.loadFrames();
}
},
);
this.eventBus.subscribe(
'open-popup-settings',
{
source: this,
function: (config) => {
this.selectTab(config.tab)
}
}
)
this.comms = new CommsClient('popup-port', this.logger, this.eventBus);
this.eventBus.setComms(this.comms);
@ -253,6 +253,9 @@ export default {
this.requestSite();
await this.sleep(5000);
}
} catch (e) {
console.error('[Popup.vue::created()] An error happened:', e)
}
},
async updated() {
const body = document.getElementsByTagName('body')[0];
@ -287,6 +290,7 @@ export default {
// CSM.port.postMessage({command: 'get-current-site'});
this.eventBus.send(
'get-current-site',
{},
{
comms: {forwardTo: 'active'}
}
@ -301,62 +305,29 @@ export default {
selectTab(tab) {
this.selectedTab = tab;
},
processReceivedMessage(message, port) {
this.logger.log('info', 'popup', '[popup::processReceivedMessage] received message:', message)
if (message.command === 'set-current-site'){
if (this.site) {
if (!this.site.host) {
// dunno why this fix is needed, but sometimes it is
this.site.host = site.tabHostname;
}
}
this.site = message.site;
// update activeSites
// this.activeSites = this.activeSites.filter(x => x.host !== message.site);
// add current site
// this.activeSites = unshift({
// host: message.site.host,
// isIFrame: false, // currently unused
// });
this.selectedSite = this.selectedSite || message.site.host;
this.loadFrames(this.site);
}
return true;
},
isDefaultFrame(frameId) {
return frameId === '__playing' || frameId === '__all';
},
loadHostnames() {
this.activeHosts = this.site.hostnames;
},
loadFrames() {
this.activeSites = [{
this.activeFrames = [{
host: this.site.host,
isIFrame: false, // not used tho. Maybe one day
}];
this.selectedSite = this.selectedSite || this.site.host;
// for (const frame in videoTab.frames) {
// this.activeFrames.push({
// id: `${this.site.id}-${frame}`,
// label: videoTab.frames[frame].host,
// ...this.frameStore[frame],
// })
// // only add each host once at most
// if (!this.activeSites.find(x => x.host === videoTab.frames[frame].host)) {
// this.activeSites.push({
// host: videoTab.frames[frame].host,
// isIFrame: undefined // maybe one day
// });
// }
// }
// update whether video tab can be shown
// this.updateCanShowVideoTab();
for (const frame in this.site.frames) {
if (!this.activeFrames.find(x => x.host === this.site.frames[frame].host)) {
this.activeFrames.push({
id: `${this.site.id}-${frame}`,
label: this.site.frames[frame].host,
host: this.site.frames[frame].host,
...this.site.frames[frame],
...this.settings.active.sites[this.site.frames[frame].host]
})
};
}
},
getRandomColor() {
return `rgb(${Math.floor(Math.random() * 128)}, ${Math.floor(Math.random() * 128)}, ${Math.floor(Math.random() * 128)})`;
@ -616,4 +587,10 @@ pre {
h1 {
margin: 0; padding: 0; font-weight: 400; font-size:24px;
}
.window-content {
height: 100%;
width: 100%;
overflow: auto;
}
</style>

View File

@ -3,6 +3,8 @@ import GlobalFrame from './GlobalFrame';
import mdiVue from 'mdi-vue/v3';
import * as mdijs from '@mdi/js';
import './src/res-common/common.scss';
// NOTE — this is in-player interface for ultrawidify
// it is injected into the page in UI.init()

View File

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en" style="position: relative">
<html lang="en" style="position: relative; height: 600px">
<head>
<meta charset="UTF-8">
<title>Title</title>
@ -10,9 +10,9 @@
<% } %>
</head>
<body
style="width: 100%; height: 100%; overflow-x: hidden; min-width: 750px"
style="width: 100%; height: 100vh; overflow-x: hidden; min-width: 750px;"
>
<div id="app">
<div id="app" style="max-height: 100vh;">
</div>
<script src="csui-popup.js"></script>

View File

@ -3,6 +3,8 @@ import PlayerOverlay from './PlayerOverlay';
import mdiVue from 'mdi-vue/v3';
import * as mdijs from '@mdi/js';
import './src/res-common/common.scss';
// NOTE — this is in-player interface for ultrawidify
// it is injected into the page in UI.init()

View File

@ -23,7 +23,7 @@
</template>
<script>
import Logger from '../../../ext/lib/Logger';
export default {
components: {

View File

@ -5,6 +5,10 @@
// @import "form.scss";
* {
box-sizing: border-box;
}
body {
background-color: $background-primary;
color: $text-normal;
@ -19,7 +23,7 @@ body {
}
.main-window::before {
content: ' backdrop-filter machine broke :( googlo pls fix';
content: ' ';
color: #000;
position: absolute;
left: -0.5rem;
@ -32,7 +36,7 @@ body {
}
/* STANDARD WIDTHS AND HEIGHTS */
.w100 {
.w100, .w-full {
width: 100%;
}
@ -248,6 +252,7 @@ small {
background-color: #410 !important;
}
button,
.button {
/*display: inline-block;*/
// padding-top: 8px;
@ -261,6 +266,27 @@ small {
text-align: center;
cursor: pointer;
user-select: none;;
background-color: rgba(11,11,11,0.75);
padding: 0.5rem 2rem;
background-color: rgba(11, 11, 11, 0.5);
border: 1px solid transparent;
user-select: none !important;
&.primary {
background-color: #fa6;
color: #000;
}
&.danger {
background-color: #ff2211 !important;
color:#000;
}
&.disabled {
filter: saturate(0%);
pointer-events: none;
}
}
@ -341,6 +367,10 @@ small {
padding-right: 10px;
}
.info-button {
color: $info-color;
border: 1px solid $info-color;
}
.info {
color: $info-color;
padding-left: 35px;

View File

@ -14,22 +14,25 @@
flex: 1 1 auto;
}
.flex-grow {
.flex-grow, .grow {
flex-grow: 1;
}
.flex-nogrow {
.flex-nogrow, .grow-0 {
flex-grow: 0;
}
.flex-shrink {
.flex-shrink, .shrink {
flex-shrink: 1;
}
.flex-noshrink {
.flex-noshrink, .shrink-0 {
flex-shrink: 0;
}
.items-center {
align-items: center;
}
.items-baseline {
align-items: baseline;
}
.flex-wrap {
flex-wrap: wrap;

View File

@ -30,6 +30,9 @@
<div
v-for="tab of tabs"
:key="tab.id"
>
<div
v-if="!tab.hidden"
class="tab"
:class="{
'active': tab.id === selectedTab,
@ -49,6 +52,7 @@
</div>
</div>
</div>
</div>
<div class="content flex flex-col">
<!-- autodetection warning -->
@ -95,6 +99,7 @@
:settings="settings"
:siteSettings="siteSettings"
:site="site"
:enableSettingsEditor="true"
></BaseExtensionSettings>
<AutodetectionSettingsPanel
v-if="selectedTab === 'autodetectionSettings'"
@ -168,10 +173,9 @@ export default {
{id: 'playerDetection', label: 'Player detection', icon: 'television-play'},
{id: 'autodetectionSettings', label: 'Autodetection options', icon: 'auto-fix'},
// {id: 'advancedOptions', label: 'Advanced options', icon: 'cogs' },
// {id: 'debugging', label: 'Debugging', icon: 'bug-outline' }
{id: 'changelog', label: 'What\'s new', icon: 'alert-decagram' },
{id: 'about', label: 'About', icon: 'information-outline'},
// {id: 'resetBackup', label: 'Reset and backup', icon: 'file-restore-outline'},
{id: 'debugging', label: 'Debugging', icon: 'bug-outline', hidden: true},
],
selectedTab: 'extensionSettings',
BrowserDetect: BrowserDetect,
@ -196,11 +200,13 @@ export default {
}
},
created() {
this.settings.listenAfterChange(this.setDebugTabVisibility);
if (this.defaultTab) {
this.selectedTab = this.defaultTab;
}
this.siteSettings = this.settings.getSiteSettings(this.site);
this.tabs.find(x => x.id === 'changelog').highlight = !this.settings.active.whatsNewChecked;
this.tabs.find(x => x.id === 'changelog').highlight = !this.settings.active?.whatsNewChecked;
this.eventBus.subscribe(
'uw-show-ui',
@ -213,8 +219,10 @@ export default {
},
}
)
this.setDebugTabVisibility();
},
destroyed() {
this.settings.removeListenerAfterChange(this.setDebugTabVisibility);
this.eventBus.unsubscribeAll(this);
},
methods: {
@ -230,6 +238,12 @@ export default {
setPreventClose(bool) {
this.preventClose = bool;
this.$emit('preventClose', bool);
},
setDebugTabVisibility() {
const debugTab = this.tabs.find( x => x.id === 'debugging');
if (debugTab) {
debugTab.hidden = !this.settings.active.ui.devMode;
}
}
}
}
@ -371,7 +385,7 @@ export default {
padding: 2rem;
font-size: 1.5rem;
height: 4rem;
height: 6rem;
border-bottom: 1px solid rgba(128, 128, 128, 0.5);
border-top: 1px solid rgba(128, 128, 128, 0.5);

View File

@ -10,7 +10,7 @@
<div class="w-[1/2]" style="width: 50%">
<h2>Report a problem</h2>
<p>
You may report <strike>undocumented features</strike> bugs using one of the following options (in order of preference):
Please report <strike>undocumented features</strike> bugs using one of the following options (in order of preference):
</p>
<ul>
<li> <a target="_blank" href="https://github.com/tamius-han/ultrawidify/issues"><b>Github (preferred)</b></a><br/></li>

View File

@ -1,5 +1,5 @@
<template>
<div class="alignment-box">
<div class="alignment-box" :class="{large: large}">
<div
class="col top left"
@click="align(VideoAlignment.Left, VideoAlignment.Top)"
@ -60,10 +60,10 @@
<script>
import VideoAlignmentType from '../../../common/enums/VideoAlignmentType.enum';
export default {
props: [
'eventBus'
'eventBus',
'large',
],
data() {
return {
@ -89,6 +89,14 @@ export default {
grid-template-columns: repeat(3, 1fr);
gap: 0.5rem;
&.large {
max-width: 15rem;
.col {
width: 4rem;
height: 4rem;
}
}
.col {
display: flex;

View File

@ -234,6 +234,7 @@
/>
<input
v-model="settings.active.arDetect.timers.playing"
@change="setArCheckFrequency($event.target.value)"
class="input"
type="text"
>
@ -244,10 +245,10 @@
<div class="field">
<div class="label">Frame extraction canvas type:</div>
<div class="select">
<select v-model="settings.active.arDetect.aardType">
<select v-model="settings.active.arDetect.aardType" @change="settings.saveWithoutReload">
<option value="auto">Automatic</option>
<option value="webgl">WebGL only</option>
<option value="fallback">Legacy / fallback</option>
<option value="legacy">Legacy / fallback</option>
</select>
</div>
</div>
@ -264,6 +265,34 @@
</div>
</div>
</div>
<div v-if="settings.active.ui.devMode" class="settings-segment">
<p>
<b>Debug options</b>
</p>
<div class="flex flex-row">
<div>
<div>
<button @click="eventBus.sendToTunnel('aard-enable-debug', true)">Show debug overlay</button>
</div>
<div>
<div class="label">Show debug overlay on startup</div>
<input
type="checkbox"
v-model="settings.active.ui.dev.aardDebugOverlay.showOnStartup"
@change="settings.saveWithoutReload"
>
</div>
</div>
<div>
<JsonEditor
v-model="settingsJson"
>
</JsonEditor>
<button @click="saveDebugUiSettings">Save debug UI settings</button>
</div>
</div>
</div>
</div>
</div>
</div>
@ -280,14 +309,15 @@ import AspectRatioType from '../../../common/enums/AspectRatioType.enum';
import StretchType from '../../../common/enums/StretchType.enum';
import CropModePersistence from '../../../common/enums/CropModePersistence.enum';
import AlignmentOptionsControlComponent from './AlignmentOptionsControlComponent.vue';
import JsonEditor from '@csui/src/components/JsonEditor';
export default {
data() {
return {
exec: null,
performanceData: {},
graphRefreshInterval: undefined,
}
components: {
ShortcutButton,
EditShortcutButton,
Button,
AlignmentOptionsControlComponent,
JsonEditor
},
mixins: [
],
@ -297,6 +327,16 @@ export default {
'eventBus',
'site'
],
data() {
return {
exec: null,
performanceData: {},
graphRefreshInterval: undefined,
settingsJson: {},
}
},
computed: {
},
created() {
this.eventBus.subscribe(
'uw-config-broadcast',
@ -306,24 +346,15 @@ export default {
}
);
},
destroyed() {
this.eventBus.unsubscribeAll(this);
},
mounted() {
this.eventBus.sendToTunnel('get-aard-timing');
this.graphRefreshInterval = setInterval(() => this.eventBus.sendToTunnel('get-aard-timing'), 500);
this.resetSettingsEditor();
},
destroyed() {
this.eventBus.unsubscribeAll(this);
clearInterval(this.graphRefreshInterval);
},
components: {
ShortcutButton,
EditShortcutButton,
Button,
AlignmentOptionsControlComponent
},
computed: {
},
methods: {
async openOptionsPage() {
BrowserDetect.runtime.openOptionsPage();
@ -340,7 +371,15 @@ export default {
this.$nextTick( () => this.$forceUpdate() );
}
},
resetSettingsEditor() {
this.settingsJson = JSON.parse(JSON.stringify(this.settings?.active.ui.dev.aardDebugOverlay ?? {}));
},
saveDebugUiSettings() {
this.settings.active.ui.dev.aardDebugOverlay = JSON.parse(JSON.stringify(this.settingsJson));
this.settings.saveWithoutReload();
}
},
}
</script>

View File

@ -1,5 +1,6 @@
<template>
<div class="flex flex-col w-100">
<div class="flex flex-row w-full h-full">
<div class="flex flex-col w-full">
<!-- TAB ROW -->
<div class="flex flex-row">
@ -8,29 +9,27 @@
:class="{'active': tab === 'siteSettings'}"
@click="setTab('siteSettings')"
>
Settings for current site<br/>
Current site<br/>
<small>{{ site }}</small>
</div>
<div
v-if="hosts"
class="tab"
:class="{'active': tab === 'extensionSettings'}"
@click="setTab(tab = 'extensionSettings')"
:class="{'active': tab === 'embeddedSites'}"
@click="setTab(tab = 'embeddedSites')"
>
Default settings for extension
Embedded content ({{hosts?.length}} {{hosts?.length === 1 ? 'site' : 'sites'}})
</div>
<div
class="tab"
:class="{'active': tab === 'otherSites'}"
@click="setTab(tab = 'otherSites')"
>
Settings for other sites
Defaults & other sites
</div>
</div>
<template v-if="tab === 'siteSettings' && siteSettings">
<!-- <div class="button">
Reset settings for site
</div> -->
<SiteExtensionSettings
v-if="settings"
:settings="settings"
@ -39,13 +38,12 @@
></SiteExtensionSettings>
</template>
<template v-if="tab === 'extensionSettings' && globalSettings">
<SiteExtensionSettings
<template v-if="hosts && tab === 'embeddedSites' && globalSettings">
<FrameSiteSettings
v-if="settings"
:hosts="hosts"
:settings="settings"
:siteSettings="globalSettings"
:isDefaultConfiguration="true"
></SiteExtensionSettings>
></FrameSiteSettings>
</template>
<template v-if="tab === 'otherSites'">
@ -56,46 +54,141 @@
</OtherSiteSettings>
</template>
<!-- Reset options -->
<div class="flex flex-col" style="margin-top: 2rem">
<h2>Reset and backup</h2>
<p>
Pressing the button will reset settings to default without asking.
</p>
<p><small>Settings import must be done from in-player UI.</small></p>
<div class="flex flex-row w-full">
<UploadJsonFileButton
class="flex-grow"
@importedJson="handleImportedSettings"
@error="handleSettingsImportError"
>
Import settings
</UploadJsonFileButton>
<Popup
v-if="importSettingDialogConfig.visible"
title="Overwrite existing settings?"
message="Importing settings from a file will overwrite existing settings. Continue?"
confirmButtonText="Import settings"
cancelButtonText="Cancel"
@onConfirm="importSettingDialogConfig.confirm"
@onCancel="importSettingDialogConfig.reject"
>
</Popup>
<button class="flex-grow" @click="exportSettings()">Export settings</button>
</div>
<div></div>
<ConfirmButton
class="danger"
dialogType="danger"
@onConfirmed="resetSettings"
>
Reset settings
</ConfirmButton>
<div v-if="enableSettingsEditor" class="field">
<div class="label">Show developer options</div>
<input
type="checkbox"
v-model="settings.active.ui.devMode"
@change="settings.saveWithoutReload"
>
</div>
</div>
</div>
<div v-if="enableSettingsEditor && settings.active.ui.devMode" class="h-full grow">
<h2>Settings editor</h2>
<div class="flex flex-row w-full">
<div class="flex flex-row items-center">
<div>Enable save button:</div>
<input v-model="allowSettingsEditing" type="checkbox">
</div>
<div class="grow">
</div>
<div>
<div v-if="editorSaveFinished">Settings saved ...</div>
<button v-else
class="danger"
:class="{'disabled': !allowSettingsEditing}"
:disabled="!allowSettingsEditing"
@click="() => saveSettingsChanges()"
>
Save
</button>
</div>
<button @click="resetSettingsEditor">
Cancel
</button>
</div>
<div>
<JsonEditor
v-model="settingsJson"
>
</JsonEditor>
</div>
<h2>Settings snapshots</h2>
<div class="flex flex-col">
<div v-for="(snapshot, index) of settingsSnapshots" :key="snapshot.createdAt">
<small>{{new Date(snapshot.createdAt).toISOString()}}</small>
<div class="flex flex-row">
<div class="grow">
{{snapshot.name}}
</div>
<div v-if="settings.isAutomatic">(auto)</div>
<div v-if="settings.isAutomatic && settings.isProtected">(protected)</div>
<div v-if="settings.default">(default)</div>
</div>
<div>
<button @click="() => markDefaultSnapshot(index)"><template v-if="settings.isDefault">Revoke default</template><template v-else>Make default</template></button>
<button v-if="settings.isAutomatic" @click="() => toggleSnapshotProtection(index)">Toggle protection</button>
<button @click="() => deleteSnapshot(index)">Delete snapshot</button>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import SiteExtensionSettings from './PanelComponents/ExtensionSettings/SiteExtensionSettings.vue';
import FrameSiteSettings from './PanelComponents/ExtensionSettings/FrameSiteSettings.vue';
import OtherSiteSettings from './PanelComponents/ExtensionSettings/OtherSiteSettings.vue';
import Popup from '@csui/src/components/Popup';
import ConfirmButton from '@csui/src/components/ConfirmButton';
import UploadJsonFileButton from '@csui/src/components/UploadJsonFileButton';
import JsonEditor from '@csui/src/components/JsonEditor';
export default {
data() {
return {
tab: 'siteSettings'
}
},
mixins: [
],
props: [
'settings',
'site',
],
components: {
SiteExtensionSettings,
OtherSiteSettings,
ConfirmButton
Popup,
ConfirmButton,
UploadJsonFileButton,
JsonEditor,
FrameSiteSettings,
},
mixins: [],
props: [
'settings',
'site',
'enableSettingsEditor',
'hosts',
],
data() {
return {
tab: 'siteSettings',
importSettingDialogConfig: {visible: false},
allowSettingsEditing: false,
editorSaveFinished: false,
settingsJson: {},
settingsSnapshots: []
}
},
computed: {
globalSettings() {
@ -107,17 +200,113 @@ export default {
}
return null;
},
resetSettings() {
this.settings.active = JSON.parse(JSON.stringify(this.settings.default));
this.settings.saveWithoutReload();
}
},
mounted() {
this.resetSettingsEditor();
this.loadSettingsSnapshots();
},
methods: {
setTab(tab) {
this.tab = tab;
},
//#region settings management
/**
* Exports extension settings into a json file.
*/
exportSettings() {
const settingBlob = new Blob(
[ JSON.stringify(this.settings.active, null, 2) ],
{ type: "application/json"}
);
const url = window.URL.createObjectURL(settingBlob);
const a = document.createElement("a");
a.href = url;
a.download = "ultrawidify-settings.json";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
},
handleImportedSettings(newSettings) {
this.importSettingDialogConfig = {
visible: true,
confirm: () => {
this.settings.active = newSettings;
this.settings.saveWithoutReload();
this.importSettingDialogConfig = {visible: false};
this.resetSettingsEditor();
},
reject: () => {
this.importSettingDialogConfig = {visible: false};
}
}
},
handleSettingsImportError(error) {
console.error('Error importing settings:', error);
},
/**
* Resets settings to default
*/
resetSettings() {
this.settings.active = JSON.parse(JSON.stringify(this.settings.default));
this.settings.saveWithoutReload();
this.resetSettingsEditor();
},
async saveSettingsChanges() {
if (this.allowSettingsEditing) {
const currentVersion = this.settings.active?.version;
this.settings.active = this.settingsJson;
if (currentVersion !== this.settingsJson.version) {
await this.settings.save({forcePreserveVersion: true});
return;
} else {
await this.settings.saveWithoutReload();
}
this.resetSettingsEditor();
this.editorSaveFinished = true;
setTimeout(() => {
this.editorSaveFinished = false;
}, 3000);
}
},
resetSettingsEditor() {
this.settingsJson = JSON.parse(JSON.stringify(this.settings?.active ?? {}));
},
//#endregion
//#region settings snapshot management
async loadSettingsSnapshots() {
this.settingsSnapshots = await this.settings.snapshotManager.listSnapshots();
},
async markDefaultSnapshot(index) {
await this.settings.snapshotManager.setDefaultSnapshot(index, !this.settingsSnapshots[index].isDefault);
await this.loadSettingsSnapshots();
this.settings.active.dev.loadFromSnapshot = this.settingsSnapshots[index].isDefault;
this.saveSettingsChanges();
},
async toggleSnapshotProtection(index) {
await this.settings.snapshotManager.setSnapshotAsProtected(index, !this.settingsSnapshots[index].isProtected);
await this.loadSettingsSnapshots();
},
async deleteSnapshot(index) {
await this.settings.snapshotManager.deleteSnapshot(index);
await this.loadSettingsSnapshots();
},
}
//#endregion
}
</script>

View File

@ -1,31 +1,36 @@
<template>
<div class="flex flex-col w-full h-full gap-2">
<div class="flex flex-row gap-8 bg-black flex-wrap">
<div class="min-w-[400px] max-w-[520px] grow-1 shrink-1">
<div class="flex flex-row gap-8 bg-black flex-wrap w-full">
<div class="min-w-[400px] max-w-[520px] grow shrink">
<h1>What's new</h1>
<!-- <p>Full changelog for older versions <a href="https://github.com/tamius-han/ultrawidify/blob/master/CHANGELOG.md" target="_blank">is available here</a>.</p> -->
<h2>6.2.4</h2>
<h2>6.3.0</h2>
<ul>
<li>[<a href="https://github.com/tamius-han/ultrawidify/issues/264" target="_blank">#264</a>] Fixed issue with white screen that affected <i>some</i> youtube users.<br/>Special thanks to <a href="https://github.com/SnowyOwlNugget">SnowyOwlNugget</a>, who instead of whining provided the necessary information.</li>
<li>Minor updates to the settings</li>
<li>Switching between full screen, theater, and normal player now correctly enables and disables the extension according to the settings.</li>
<li>Does anyone even read these?</li>
<li>By default, extension is now only enabled for sites with 'official' and 'community' support for new installs. Extension will need to be manually enabled for other sites.</li>
<li>Gussief1, you owe me 5 bucks. Time to put your money where your mouth is. Paypal is <a href="https://www.paypal.com/paypalme/tamius" target="_blank">here.</a></li>
<li>Added background to in-player UI due to chrome bug affecting <code>backdrop-filter</code> inside transparent iframes.</li>
<li>Automatic aspect ratio detection: do not apply negative aspect ratios</li>
<li>Keyboard zoom now works</li>
<li><code>www.youtube-nocookie.com</code> has been added to the "officially supported" list</li>
<li>Fixed the bug where UI would sometimes refuse to stay hidden</li>
<li>New experimental zoom options</li>
<li>Subdomains now inherit same settings as their parent domain by default</li>
<li>Extension attempts to detect embedded content.</li>
</ul>
</div>
<div style="width: 1rem; height: 0px;"></div>
<div class="min-w-[400px] max-w-[520px] grow shrink">
<h2>Report a problem</h2>
<p>
Planned future updates <i>before</i> Tam vanishes due to convention season obligations:
Please report <strike>undocumented features</strike> bugs using one of the following options (in order of preference):
</p>
<ul>
<li>Re-implement manual zoom</li>
<li>It appears that ExtConfPatch for version 6.0.0 keeps getting re-applied due to a bug, but my RAID ran out for this release.</li>
<li> <a target="_blank" href="https://github.com/tamius-han/ultrawidify/issues"><b>Github (preferred)</b></a><br/></li>
<li>Email: <a target="_blank" :href="mailtoLink">tamius.han@gmail.com</a></li>
</ul>
</div>
<div class="min-w-[400px] max-w-[520px] grow-1 shrink-1">
<p>
When reporting bugs, please include extension version, whether you installed the extension from, and description of your problem.
</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<h2>Thank you monies</h2>
<p>
If you think I deserve money for the work I did up to this point, you can bankroll my caffeine addiction.
@ -38,11 +43,23 @@
</div>
</template>
<script>
import BrowserDetect from '@src/ext/conf/BrowserDetect';
export default({
props: [
'settings'
],
data() {
return {
BrowserDetect: BrowserDetect,
// reminder webextension-polyfill doesn't seem to work in vue!
addonVersion: BrowserDetect.firefox ? chrome.runtime.getManifest().version : chrome.runtime.getManifest().version,
addonSource: BrowserDetect.processEnvVersion,
mailtoLink: '',
redditLink: '',
showEasterEgg: false,
}
},
mounted() {
this.settings.active.whatsNewChecked = true;
this.settings.saveWithoutReload();
@ -57,6 +74,10 @@ export default({
flex-direction: row;
}
.grow {
flex-grow: 1;
}
p, li {
font-size: 1rem;
}

View File

@ -6,20 +6,12 @@
<div>Logger configuration:</div>
<template v-if="loggerPanel.pasteConfMode">
</template>
<template v-else>
<JsonObject
label="logger-settings"
:value="currentSettings"
:ignoreKeys="{'allowLogging': false}"
@change="updateSettingsUi"
></JsonObject>
</template>
<JsonEditor
v-model="lastSettings"
></JsonEditor>
<div class="flex flex-row flex-end">
<div class="button" @click="restoreLoggerSettings()">
<div class="button" @click="getLoggerSettings()">
Revert
</div>
<div class="button button-primary" @click="saveLoggerSettings()">
@ -35,12 +27,12 @@
</template>
<script>
import JsonObject from '../components/JsonEditor/JsonObject.vue'
import Logger, { baseLoggingOptions } from '../../../ext/lib/Logger';
import { LogAggregator, BLANK_LOGGER_CONFIG } from '@src/ext/lib/logging/LogAggregator';
import JsonEditor from '@csui/src/components/JsonEditor';
export default {
components: {
JsonObject
JsonEditor
},
data() {
return {
@ -87,38 +79,23 @@ export default {
...this.loggerPanelRotation[Math.floor(Math.random() * this.loggerPanelRotation.length)],
pasteConfMode: false,
};
this.loadDefaultConfig();
this.getLoggerSettings();
},
methods: {
loadDefaultConfig() {
this.lastSettings = baseLoggingOptions;
this.parsedSettings = JSON.stringify(this.lastSettings, null, 2) || '';
this.currentSettings = JSON.parse(JSON.stringify(this.lastSettings));
this.lastSettings = JSON.parse(JSON.stringify(BLANK_LOGGER_CONFIG));
},
async getLoggerSettings() {
this.lastSettings = await Logger.getConfig() || baseLoggingOptions;
this.parsedSettings = JSON.stringify(this.lastSettings, null, 2) || '';
this.currentSettings = JSON.parse(JSON.stringify(this.lastSettings));
this.lastSettings = await LogAggregator.getConfig() || BLANK_LOGGER_CONFIG;
},
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 saveLoggerSettings() {
console.log('Saving logger settings', this.lastSettings);
await LogAggregator.saveConfig({...this.lastSettings});
console.log('[ok] logger settings saved');
},
async startLogging(){
this.logStringified = undefined;
await Logger.saveConfig({...this.lastSettings, allowLogging: true});
await LogAggregator.saveConfig({...this.lastSettings});
window.location.reload();
},
}

View File

@ -1,59 +0,0 @@
<template>
<div>
<h1>In-player UI</h1>
<div
class="button b3"
style="margin: 16px; padding: 4px;"
@click="showInPlayerUi()"
>
Show settings window
</div>
<!-- <p></p>
<p></p>
<p>In-player UI should show and hide automatically as you start or stop moving your mouse inside the player window.</p>
<p>Note that by default, in-player UI may not show if player window is not big enough.</p> -->
</div>
</template>
<script>
import UIProbeMixin from '../utils/UIProbeMixin';
export default {
mixins: [
UIProbeMixin
],
props: [
'eventBus',
],
data() {
return {
pageData: {
pcsDark: 'x',
pcsLight: 'x',
colorScheme: 'x'
},
}
},
created() {
this.eventBus.subscribeMulti(
{
'uw-page-stats': {
function: (data) => {
console.log('got page statss:', data);
this.pageData = data;
}
}
},
this
);
this.eventBus.send('uw-get-page-stats', {}, {comms: {forwardTo: 'active'}});
},
methods: {
showInPlayerUi() {
this.eventBus.send('uw-set-ui-state', {globalUiVisible: true}, {comms: {forwardTo: 'active'}});
}
}
}
</script>

View File

@ -0,0 +1,107 @@
<template>
<div class="">
<template v-if="!selectedSite">
<div style="margin: 1rem 0rem" class="w-full">
<div class="flex flex-row items-baseline">
<div style="margin-right: 1rem">Search for site:</div>
<div class="input flex-grow">
<input v-model="siteFilter" />
</div>
</div>
</div>
<div v-for="host of hosts" :key="host" @click="selectedSite = host" class="flex flex-col container pointer hoverable" style="margin-top: 4px; padding: 0.5rem 1rem;">
<SiteListItem
:host="host"
:settings="settings"
></SiteListItem>
</div>
</template>
<template v-if="selectedSite">
<div class="flex flex-row container" style="align-items: center; color: #dedede; margin-top: 1rem;">
<div @click="selectedSite = null" class="pointer button-hover" style=" font-size: 2em; padding: 0.5rem; margin-right: 1em;">
</div>
<div>
Editing {{ selectedSite === '@global' ? 'default settings' : selectedSite }}
</div>
</div>
<div>
<SiteExtensionSettings
v-if="selectedSiteSettings"
:settings="settings"
:siteSettings="selectedSiteSettings"
:isDefaultConfiguration="selectedSite === '@global'"
></SiteExtensionSettings>
</div>
</template>
</div>
</template>
<script>
import SiteExtensionSettings from './SiteExtensionSettings.vue';
import SiteListItem from './SiteListItem.vue';
export default {
components: {
SiteExtensionSettings,
SiteListItem,
},
props: [
'settings',
'hosts',
],
data() {
return {
selectedSite: null,
siteFilter: '',
filteredSites: []
}
},
computed: {
sites() {
if (!this.settings?.active?.sites) {
return [];
} else {
const sites = [];
for (const siteKey in this.settings.active.sites) {
if (!siteKey.startsWith('@') && (!this.siteFilter.trim() || siteKey.includes(this.siteFilter))) {
sites.push({
key: siteKey,
...this.settings.active.sites[siteKey]
})
}
};
sites.sort((a, b) => {
const cmpa = a.key.replace('www.', '');
const cmpb = b.key.replace('www.', '');
return cmpa < cmpb ? -1 : 1;
});
return sites;
}
},
selectedSiteSettings() {
return this.settings?.getSiteSettings(this.selectedSite) ?? null;
}
},
methods: {
}
}
</script>
<style lang="scss" src="../../../../res/css/flex.scss" scoped></style>
<style lang="scss" src="@csui/src/res-common/panels.scss" scoped></style>
<style lang="scss" src="@csui/src/res-common/common.scss" scoped></style>
<style lang="scss" scoped>
.hoverable {
border: 1px solid #333;
&:hover {
border: 1px solid #fa6;
color: rgb(255, 231, 212);
background-color: rgba(#fa6, 0.125);
}
}
</style>

View File

@ -1,16 +1,51 @@
<template>
<div class="">
<template v-if="!selectedSite">
<div class="flex flex-col container pointer hoverable" style="margin-top: 1rem; padding: 0.5rem 1rem;" @click="selectedSite = '@global'" >
<div class="flex flex-row">
<div class="flex-grow pointer">
<b style="color: #fa6">Default settings</b>
<!-- <span :style="getSiteTypeColor('@global')"> -->
<!-- (config: {{site.type ?? 'unknown'}}) -->
<!-- </span> -->
</div>
<div>Edit</div>
</div>
<div class="flex flex-row">
<small>
<span style="text-transform: uppercase; font-size: 0.9rem">Enable extension: <span style="font-size: 0.9rem;" :style="getSiteEnabledColor('@global', 'enable')">{{ getSiteEnabledModes('@global', 'enable') }}</span></span>&nbsp;<br/>
Autodetection: <span :style="getSiteEnabledColor('@global', 'enableAard')"><small>{{ getSiteEnabledModes('@global', 'enableAard') }}</small></span>;&nbsp;
Keyboard shortcuts: <span :style="getSiteEnabledColor('@global', 'enableKeyboard')"><small>{{ getSiteEnabledModes('@global', 'enableKeyboard') }}</small></span>;
In-player UI: <span :style="getSiteEnabledColor('@global', 'enableUI')"><small>{{ getSiteEnabledModes('@global', 'enableUI') }}</small></span>;
</small>
</div>
</div>
<div style="margin-top: 1rem; margin-bottom: 1rem;">
<div class="info" style="float: none">
<b>NOTE:</b> Sites not on this list use default extension settings.
</div>
<div v-for="site of sites" :key="site.key" @click="selectedSite = site.key" class="flex flex-col container pointer" style="margin-top: 4px; padding: 0.5rem 1rem;">
</div>
<div class="w-full text-center" style="margin-bottom: -1.25rem">
<b>Other sites</b>
</div>
<div style="margin: 1rem 0rem" class="w-full">
<div class="flex flex-row items-baseline">
<div style="margin-right: 1rem">Search for site:</div>
<div class="input flex-grow">
<input v-model="siteFilter" />
</div>
</div>
</div>
<div v-for="site of sites" :key="site.key" @click="selectedSite = site.key" class="flex flex-col container pointer hoverable" style="margin-top: 4px; padding: 0.5rem 1rem;">
<div class="flex flex-row">
<div class="flex-grow pointer">
<b>{{ site.key }}</b>
<span :style="getSiteTypeColor(site.type)">
(config: {{site.type ?? 'unknown'}})
</span></div>
</span>
</div>
<div>Edit</div>
</div>
<div class="flex flex-row">
@ -18,6 +53,7 @@
Enabled: <span :style="getSiteEnabledColor(site.key, 'enable')"><small>{{ getSiteEnabledModes(site.key, 'enable') }}</small></span>;&nbsp;
Aard <span :style="getSiteEnabledColor(site.key, 'enableAard')"><small>{{ getSiteEnabledModes(site.key, 'enableAard') }}</small></span>;&nbsp;
kbd: <span :style="getSiteEnabledColor(site.key, 'enableKeyboard')"><small>{{ getSiteEnabledModes(site.key, 'enableKeyboard') }}</small></span>
UI: <span :style="getSiteEnabledColor(site.key, 'enableUI')"><small>{{ getSiteEnabledModes(site.key, 'enableUI') }}</small></span>
</small>
</div>
</div>
@ -28,7 +64,7 @@
</div>
<div>
Editing {{ selectedSite }}
Editing {{ selectedSite === '@global' ? 'default settings' : selectedSite }}
</div>
</div>
<div>
@ -36,7 +72,7 @@
v-if="selectedSiteSettings"
:settings="settings"
:siteSettings="selectedSiteSettings"
:isDefaultConfiguration="false"
:isDefaultConfiguration="selectedSite === '@global'"
></SiteExtensionSettings>
</div>
</template>
@ -51,6 +87,8 @@ export default {
data() {
return {
selectedSite: null,
siteFilter: '',
filteredSites: []
}
},
props: [
@ -66,7 +104,7 @@ export default {
} else {
const sites = [];
for (const siteKey in this.settings.active.sites) {
if (!siteKey.startsWith('@')) {
if (!siteKey.startsWith('@') && (!this.siteFilter.trim() || siteKey.includes(this.siteFilter))) {
sites.push({
key: siteKey,
...this.settings.active.sites[siteKey]
@ -120,3 +158,15 @@ export default {
<style lang="scss" src="../../../../res/css/flex.scss" scoped></style>
<style lang="scss" src="@csui/src/res-common/panels.scss" scoped></style>
<style lang="scss" src="@csui/src/res-common/common.scss" scoped></style>
<style lang="scss" scoped>
.hoverable {
border: 1px solid #333;
&:hover {
border: 1px solid #fa6;
color: rgb(255, 231, 212);
background-color: rgba(#fa6, 0.125);
}
}
</style>

View File

@ -8,7 +8,7 @@
</div>
<div class="select">
<select
v-model="simpleExtensionSettings.enable"
:value="simpleExtensionSettings.enable"
@click="setExtensionMode('enable', $event)"
>
<option
@ -44,7 +44,7 @@
</div>
<!-- The rest of the menu is disabled when extension is disabled -->
<div :class="{disabled: simpleEffectiveSettings.enable === 'disabled'}">
<div :class="{disabled: simpleEffectiveSettings.enable === 'disabled' && !isDefaultConfiguration}">
<!-- Enable AARD -->
<div class="field">
<div class="label">
@ -53,7 +53,7 @@
</div>
<div class="select">
<select
v-model="simpleExtensionSettings.enableAard"
:value="simpleExtensionSettings.enableAard"
@click="setExtensionMode('enableAard', $event)"
>
<option
@ -96,7 +96,7 @@
</div>
<div class="select">
<select
v-model="simpleExtensionSettings.enableKeyboard"
:value="simpleExtensionSettings.enableKeyboard"
@click="setExtensionMode('enableKeyboard', $event)"
>
<option
@ -139,7 +139,7 @@
</div>
<div class="select">
<select
v-model="simpleExtensionSettings.enableUI"
:value="simpleExtensionSettings.enableUI"
@click="setExtensionMode('enableUI', $event)"
>
<template v-if="isDefaultConfiguration">
@ -170,7 +170,7 @@
<div class="label">Default crop:</div>
<div class="select">
<select
v-model="siteDefaultCrop"
:value="siteDefaultCrop"
@change="setOption('defaults.crop', $event)"
>
<option
@ -352,12 +352,15 @@ export default {
return '??';
}
},
mounted() {
this.forceRefreshPage();
},
methods: {
/**
* Compiles our extension settings into more user-friendly options
*/
compileSimpleSettings(component, getFor = 'site') {
console.log('compiling simple settings!', component, getFor);
let settingsData;
switch (getFor) {
case 'site':
@ -371,7 +374,7 @@ export default {
break;
}
console.log('getting data from:', settingsData);
// console.log('getting data from:', settingsData);
try {
if (
@ -386,14 +389,14 @@ export default {
&& settingsData?.[component]?.theater === ExtensionMode.Default
&& settingsData?.[component]?.fullscreen === ExtensionMode.Default
) {
console.log(
component, 'is set to default because:\n'
`\nsettingsData[${component}].normal: ${settingsData?.[component]?.normal} || component is enableUI?`, component,
`\nsettingsData[${component}].theater: ${settingsData?.[component]?.normal}`,
`\nsettingsData[${component}].fullscreen: ${settingsData?.[component]?.normal}`,
// console.log(
// component, 'is set to default because:\n',
// `\nsettingsData[${component}].normal: ${settingsData?.[component]?.normal} || component is enableUI?`, component,
// `\nsettingsData[${component}].theater: ${settingsData?.[component]?.normal}`,
// `\nsettingsData[${component}].fullscreen: ${settingsData?.[component]?.normal}`,
`\n\n(expected values:`, ExtensionMode.Default
)
// `\n\n(expected values:`, ExtensionMode.Default
// )
return 'default';
}
if (
@ -434,6 +437,7 @@ export default {
switch (componentValue) {
case 'fs':
return 'fullscreen only';
case 'enabled':
case 'theater':
return 'where possible';
case 'disabled':
@ -517,6 +521,9 @@ export default {
// we also need to force re-compute all watchers, otherwise UI will lag behind
// actual state of settings until reload
this.forceRefreshPage();
},
forceRefreshPage() {
this._computedWatchers?.simpleExtensionSettings?.run();
this._computedWatchers?.simpleDefaultSettings?.run();
this._computedWatchers?.siteDefaultCrop?.run();
@ -527,14 +534,17 @@ export default {
this.$nextTick( () => this.$forceUpdate());
},
setExtensionMode(component, event) {
const option = event.target.value;
console.log('SET EXTENSION MODE — OPTIONS:', option);
if (option === 'complex') {
return;
}
if (component === 'enable') {
if (component === 'enable' && !this.isDefaultConfiguration) {
this.setExtensionMode('enableAard', event);
this.setExtensionMode('enableKeyboard', event);

View File

@ -0,0 +1,71 @@
<template>
<div>
<div class="flex flex-row">
<div class="flex-grow pointer">
<b>{{ host }}</b>
<span :style="getSiteTypeColor(siteSettings?.data?.type)">
(config: {{siteSettings?.data?.type ?? 'unknown'}})
</span>
</div>
<div>Edit</div>
</div>
<div v-if="this.siteSettings?.usesSettingsFor">
<div v-if="this.siteSettings.usesSettingsFor === '@global'">Uses default settings</div>
<div v-else>Uses settings for: {{this.siteSettings.usesSettingsFor}}</div>
</div>
<div class="flex flex-row">
<small>
Enabled: <span :style="getSiteEnabledColor(host, 'enable')"><small>{{ getSiteEnabledModes(host, 'enable') }}</small></span>;&nbsp;
Aard <span :style="getSiteEnabledColor(host, 'enableAard')"><small>{{ getSiteEnabledModes(host, 'enableAard') }}</small></span>;&nbsp;
kbd: <span :style="getSiteEnabledColor(host, 'enableKeyboard')"><small>{{ getSiteEnabledModes(host, 'enableKeyboard') }}</small></span>
UI: <span :style="getSiteEnabledColor(host, 'enableUI')"><small>{{ getSiteEnabledModes(host, 'enableUI') }}</small></span>
</small>
</div>
</div>
</template>
<script>
import ExtensionMode from '../../../../../common/enums/ExtensionMode.enum';
export default {
data() {
return {
siteSettings: undefined,
supportType: undefined
}
},
props: [
'settings',
'host',
],
created() {
this.siteSettings = this.settings.getSiteSettings(this.host);
},
methods: {
getSiteTypeColor(siteType) {
switch (siteType) {
case 'official': return 'color: #fa6';
case 'community': return 'color: rgb(114, 114, 218)';
case 'officially-disabled': return 'color: #f00';
case 'testing': return 'color: #d81';
default: return 'color: rgb(138, 65, 126)'
};
},
getSiteEnabledColor(site, component) {
const status = this.getSiteEnabledModes(site, component);
return status === 'disabled' ? 'color: #f00' : 'color: #1f8';
},
getSiteEnabledModes(site, component) {
if (this.siteSettings?.normal === ExtensionMode.Enabled) {
return 'always';
}
if (this.siteSettings?.data[component]?.theater === ExtensionMode.Enabled) {
return 'T + FS';
}
if (this.siteSettings?.data[component]?.fullscreen === ExtensionMode.Enabled) {
return 'fullscreen';
}
return 'disabled';
}
}
}
</script>

View File

@ -3,8 +3,12 @@
<ShortcutButton
v-for="(command, index) of settings?.active.commands.crop"
class="flex b3 button"
:class="{active: editMode ? index === editModeOptions?.crop?.selectedIndex : isActiveCrop(command)}"
class="flex button"
:class="{
active: editMode ? index === editModeOptions?.crop?.selectedIndex : isActiveCrop(command),
'b3-compact': compact,
b3: !compact
}"
:key="index"
:label="command.label"
:shortcut="getKeyboardShortcutLabel(command)"
@ -134,16 +138,25 @@
</template>
<script>
import ShortcutButton from '../../../components/ShortcutButton.vue';
import EditShortcutButton from '../../../components/EditShortcutButton';
import EditModeMixin from '../../../utils/EditModeMixin';
import KeyboardShortcutParserMixin from '../../../utils/KeyboardShortcutParserMixin';
import CommsMixin from '../../../utils/CommsMixin';
import AspectRatioType from '../../../../../common/enums/AspectRatioType.enum';
import ShortcutButton from '@csui/src/components/ShortcutButton.vue';
import EditShortcutButton from '@csui/src/components/EditShortcutButton';
import EditModeMixin from '@csui/src/utils/EditModeMixin';
import KeyboardShortcutParserMixin from '@csui/src/utils/KeyboardShortcutParserMixin';
import CommsMixin from '@csui/src/utils/CommsMixin';
import AspectRatioType from '@src/common/enums/AspectRatioType.enum';
export default {
components: {
ShortcutButton,
EditShortcutButton,
},
mixins: [
// ComputeActionsMixin,
EditModeMixin,
KeyboardShortcutParserMixin,
CommsMixin
],
data() {
return {
AspectRatioType: AspectRatioType,
@ -156,23 +169,14 @@ export default {
}
}
},
mixins: [
// ComputeActionsMixin,
EditModeMixin,
KeyboardShortcutParserMixin,
CommsMixin
],
props: [
'settings', // required for buttons and actions, which are global
'siteSettings',
'eventBus',
'isEditing',
'allowSettingSiteDefault'
'allowSettingSiteDefault',
'compact',
],
components: {
ShortcutButton,
EditShortcutButton,
},
computed: {
siteDefaultCrop() {
if (!this.siteSettings) {

View File

@ -3,7 +3,10 @@
<ShortcutButton
v-for="(command, index) of settings?.active.commands.stretch"
class="b3 button"
:class="{active: editMode ? index === editModeOptions?.stretch?.selectedIndex : isActiveStretch(command)}"
:class="{active: editMode ? index === editModeOptions?.stretch?.selectedIndex : isActiveStretch(command),
'b3-compact': compact,
b3: !compact
}"
:key="index"
:label="command.label"
:shortcut="getKeyboardShortcutLabel(command)"
@ -50,11 +53,11 @@
v-model="editModeOptions.stretch.selected.arguments.limit"
>
</div>
</div>
<div class="hint">
If vertical borders would take up less than this much of screen width, the image will be stretched. If the borders are too thick, image will not be stretched.
Value of 1 means 100%. Value of 0.1 means vertical black bars can take up 10% of the width at most. There's no validation on this, use common sense.
</div>
</div>
</template>
<!-- Some options are only shown for type 5 (fixed) stretch -->
@ -70,12 +73,12 @@
@blur="editModeOptions.stretch.selected.label === 'Stretch to ...' ? editModeOptions.stretch.selected.label = `Stretch to ${editModeOptions.stretch.selected.arguments.ratio}` : null"
>
</div>
</div>
<div class="hint">
You can enter a ratio in width:height format (e.g. "21:9" or "1:2.39"), or just the factor
(in this case, "1:2.39" would become "2.39" and "21:9" would become "2.33"). You should enter
your numbers without quote marks. Number will be converted to factor form on save.
</div>
</div>
<div class="field">
<div class="label">
Label:
@ -83,10 +86,10 @@
<div class="input">
<input v-model="editModeOptions.stretch.selected.label">
</div>
</div>
<div class="hint">
Label for the button. You can make it say something other than ratio.
</div>
</div>
</template>
<!-- editing keyboard shortcuts is always allowed -->
@ -99,12 +102,12 @@
>
</EditShortcutButton>
</div>
</div>
<div class="hint">
<b>Note:</b> Your browser and OS already use certain key combinations that involve Ctrl and Meta (Windows) keys and, to a lesser extent, Alt.
The extension doesn't (and cannot) check whether the keyboard shortcut you enter is actually free for you to use. The extension also won't override
any keyboard shortcuts defined by the site itself.
</div>
</div>
<div class="flex flex-row flex-end">
<div
@ -184,7 +187,8 @@ export default {
'siteSettings',
'eventBus',
'isEditing',
'allowSettingSiteDefault'
'allowSettingSiteDefault',
'compact',
],
components: {
ShortcutButton,

View File

@ -1,28 +1,125 @@
<template>
<div class="flex flex-col">
<div class="sub-panel-content flex flex-row flex-wrap">
<ShortcutButton
v-for="(command, index) of settings?.active.commands.zoom"
class="flex button"
:class="{active: editMode ? index === editModeOptions?.zoom?.selectedIndex : isActiveZoom(command),
'b3-compact': compact,
b3: !compact
}"
:key="index"
:label="command.label"
:shortcut="getKeyboardShortcutLabel(command)"
@click="editMode ? editAction(command, index, 'zoom') : execAction(command)"
>
</ShortcutButton>
<!-- "Add new" button -->
<ShortcutButton
v-if="editMode"
class="button b3"
:class="{active: editMode ? editModeOptions?.crop?.selectedIndex === null : isActiveCrop(command)}"
label="Add new"
@click="editAction(
{action: 'set-ar-zoom', label: 'New aspect ratio', arguments: {type: AspectRatioType.Fixed}},
null,
'zoom'
)"
></ShortcutButton>
</div>
<template v-if="isEditing">
<div v-if="editMode && editModeOptions?.zoom?.selected" class="sub-panel-content">
<div class="edit-action-area-header">
<span class="text-primary">Editing options for:</span> <b>{{editModeOptions?.zoom?.selected?.label}}</b>&nbsp;
<template v-if="editModeOptions?.zoom?.selectedIndex === null && editModeOptions?.zoom?.selected?.label !== 'New aspect ratio'">(New ratio)</template>
</div>
<div class="edit-action-area">
<!-- Some options are only shown for type 4 (fixed) zooms -->
<template v-if="editModeOptions?.zoom?.selected?.arguments?.type === AspectRatioType.Fixed">
<div class="field">
<div class="label">
Ratio:
</div>
<div class="input">
<!-- We do an ugly in order to avoid spamming functions down at the bottom -->
<input
v-model="editModeOptions.zoom.selected.arguments.ratio"
@blur="editModeOptions.zoom.selected.label === 'New aspect ratio' ? editModeOptions.zoom.selected.label = editModeOptions.zoom.selected.arguments.ratio : null"
>
</div>
</div>
<div class="hint">
You can enter a ratio in width:height format (e.g. "21:9" or "1:2.39"), or just the factor
(in this case, "1:2.39" would become "2.39" and "21:9" would become "2.33"). You should enter
your numbers without quote marks. Number will be converted to factor form on save.
</div>
<div class="field">
<div class="label">
Label:
</div>
<div class="input">
<input v-model="editModeOptions.zoom.selected.label">
</div>
</div>
<div class="hint">
Label for the button. You can make it say something other than ratio.
</div>
</template>
<!-- editing keyboard shortcuts is always allowed -->
<div class="field">
<div class="label">Shortcut:</div>
<div class="">
<EditShortcutButton
:shortcut="editModeOptions?.zoom?.selected?.shortcut"
@shortcutChanged="updateSelectedShortcut($event, 'zoom')"
>
</EditShortcutButton>
</div>
</div>
<div class="hint">
<b>Note:</b> Your browser and OS already use certain key combinations that involve Ctrl and Meta (Windows) keys and, to a lesser extent, Alt.
The extension doesn't (and cannot) check whether the keyboard shortcut you enter is actually free for you to use. The extension also won't override
any keyboard shortcuts defined by the site itself.
</div>
<div class="flex flex-row flex-end">
<div
v-if="editModeOptions?.zoom?.selected?.arguments?.type === AspectRatioType.Fixed && editModeOptions?.zoom?.selectedIndex !== null"
class="button"
@click="deleteAction('zoom')"
>
<mdicon name="delete"></mdicon> Delete
</div>
<div class="flex-grow"></div>
<div class="button" @click="cancelEdit('zoom')">Cancel</div>
<div class="button" @click="saveShortcut('zoom')">
<mdicon name="floppy"></mdicon>
&nbsp;
<template v-if="editModeOptions?.zoom?.selectedIndex === null">Add</template>
<template v-else>Save</template>
</div>
</div>
</div>
</div>
</template>
<template v-else>
<!--
min, max and value need to be implemented in js as this slider
should use logarithmic scale
-->
<div class="flex flex-row flex-end">
<Button
v-if="zoomAspectRatioLocked"
label="Unlock aspect ratio"
icon="lock-open"
:fixedWidth="true"
@click="toggleZoomAr()"
>
</Button>
<Button
v-else
label="Lock aspect ratio"
icon="lock"
:fixedWidth="true"
@click="toggleZoomAr()"
>
</Button>
</div>
<div class="flex flex-row w-full" style="margin-top: 0.66rem">
<div style="position:relative;" class="grow">
<template v-if="zoomAspectRatioLocked">
<div class="slider-label">
Zoom: {{getZoomForDisplay('x')}}
</div>
<input id="_input_zoom_slider"
class="input-slider"
type="range"
@ -32,17 +129,9 @@
:value="zoom.x"
@input="changeZoom($event.target.value)"
/>
<div style="overflow: auto" class="flex flex-row">
<div class="flex flex-grow medium-small x-pad-1em">
Zoom: {{getZoomForDisplay('x')}}
</div>
<div class="flex flex-nogrow flex-noshrink medium-small">
<a class="_zoom_reset x-pad-1em" @click="resetZoom()">reset</a>
</div>
</div>
</template>
<template v-else>
<div>Horizontal zoom</div>
<div class="slider-label">Horizontal zoom: {{getZoomForDisplay('x')}}</div>
<input id="_input_zoom_slider"
class="input-slider"
type="range"
@ -53,8 +142,8 @@
@input="changeZoom($event.target.value, 'x')"
/>
<div>Vertical zoom</div>
<input id="_input_zoom_slider"
<div class="slider-label">Vertical zoom: {{getZoomForDisplay('y')}}</div>
<input id="_input_zoom_slider_2"
class="input-slider"
type="range"
step="any"
@ -63,24 +152,73 @@
:value="zoom.y"
@input="changeZoom($event.target.value, 'y')"
/>
</template>
</div>
<div style="overflow: auto" class="flex flex-row">
<div class="flex flex-grow medium-small x-pad-1em">
Zoom: {{getZoomForDisplay('x')}} x {{getZoomForDisplay('y')}}
</div>
<div class="flex flex-nogrow flex-noshrink medium-small">
<a class="_zoom_reset x-pad-1em" @click="resetZoom()">reset</a>
<div class="flex flex-row items-center justify-center" style="padding-left: 1rem">
<Button
v-if="zoomAspectRatioLocked"
icon="lock"
:iconSize="16"
:fixedWidth="true"
:noPad="true"
@click="toggleZoomAr()"
>
</Button>
<Button
v-else
icon="lock-open-variant"
:iconSize="16"
:fixedWidth="true"
:noPad="true"
@click="toggleZoomAr()"
>
</Button>
<Button
icon="restore"
:iconSize="16"
:noPad="true"
@click="resetZoom()"
></Button>
</div>
</div>
</template>
</div>
</template>
<script>
import Button from '@csui/src/components/Button.vue';
import ShortcutButton from '@csui/src/components/ShortcutButton.vue';
import EditShortcutButton from '@csui/src/components/EditShortcutButton';
import EditModeMixin from '@csui/src/utils/EditModeMixin';
import KeyboardShortcutParserMixin from '@csui/src/utils/KeyboardShortcutParserMixin';
import CommsMixin from '@csui/src/utils/CommsMixin';
import AspectRatioType from '@src/common/enums/AspectRatioType.enum';
export default {
components: {
Button,
ShortcutButton,
EditShortcutButton,
},
mixins: [
// ComputeActionsMixin,
EditModeMixin,
KeyboardShortcutParserMixin,
CommsMixin
],
props: [
'settings', // required for buttons and actions, which are global
'siteSettings',
'eventBus',
'isEditing',
'compact',
],
data() {
return {
AspectRatioType,
zoomAspectRatioLocked: true,
zoom: {
x: 0,
@ -96,20 +234,24 @@ export default {
}
}
},
mixins: [
],
props: [
'settings', // required for buttons and actions, which are global
'siteSettings',
'eventBus',
'isEditing'
],
created() {
if (this.isEditing) {
this.enableEditMode();
}
},
watch: {
isEditing(newValue, oldValue) {
if (newValue) {
this.enableEditMode();
} else {
this.disableEditMode();
}
}
},
methods: {
getZoomForDisplay(axis) {
// zoom is internally handled logarithmically, because we want to have x0.5, x1, x2, x4 ... magnifications
// spaced out at regular intervals. When displaying, we need to convert that to non-logarithmic values.
return `${(Math.pow(2, this.zoom[axis]) * 100).toFixed()}%`
},
toggleZoomAr() {
@ -125,8 +267,7 @@ export default {
// this.eventBus.send('set-zoom', {zoom: 1, axis: 'y'});
// this.eventBus.send('set-zoom', {zoom: 1, axis: 'x'});
this.eventBus?.sendToTunnel('set-zoom', {zoom: 1, axis: 'y'});
this.eventBus?.sendToTunnel('set-zoom', {zoom: 1, axis: 'x'});
this.eventBus?.sendToTunnel('set-zoom', {zoom: 1});
},
changeZoom(newZoom, axis) {
// we store zoom logarithmically on this compnent
@ -140,12 +281,14 @@ export default {
newZoom = Math.pow(2, newZoom);
if (this.zoomAspectRatioLocked) {
this.eventBus?.sendToTunnel('set-zoom', {zoom: newZoom, axis: 'y'});
this.eventBus?.sendToTunnel('set-zoom', {zoom: newZoom, axis: 'x'});
this.eventBus?.sendToTunnel('set-zoom', {zoom: newZoom});
} else {
this.eventBus?.sendToTunnel('set-zoom', {zoom: newZoom, axis: axis ?? 'x'});
this.eventBus?.sendToTunnel('set-zoom', {zoom: {[axis ?? 'x']: newZoom}});
}
},
isActiveZoom(command) {
return false;
}
}
}
@ -154,3 +297,17 @@ export default {
<style lang="scss" src="../../../../res/css/flex.scss" scoped></style>
<style lang="scss" src="@csui/src/res-common/panels.scss" scoped></style>
<style lang="scss" src="@csui/src/res-common/common.scss" scoped></style>
<style lang="scss" scoped>
.input-slider {
width: 100%;
box-sizing:border-box;
margin-right: 1rem;
margin-left: 0rem;
}
.slider-label {
margin-bottom: -0.5rem;
color: #aaa;
font-size: 0.75rem;
text-transform: uppercase;
}
</style>

View File

@ -135,6 +135,19 @@
:isEditing="true"
></StretchOptionsPanel>
</div>
<!-- ZOOM OPTIONS -->
<div>
<div class="flex flex-row">
<h3 class="mth3">ZOOM OPTIONS</h3>
</div>
<ZoomOptionsPanel
:settings="settings"
:eventBus="eventBus"
:isEditing="true"
></ZoomOptionsPanel>
</div>
</div>
</div>
@ -142,16 +155,18 @@
</template>
<script>
import Button from '../components/Button.vue'
import BrowserDetect from '../../../ext/conf/BrowserDetect';
import CropOptionsPanel from './PanelComponents/VideoSettings/CropOptionsPanel.vue'
import StretchOptionsPanel from './PanelComponents/VideoSettings/StretchOptionsPanel.vue'
import Button from '@csui/src/components/Button.vue'
import BrowserDetect from '@src/ext/conf/BrowserDetect';
import CropOptionsPanel from '@csui/src/PlayerUiPanels/PanelComponents/VideoSettings/CropOptionsPanel.vue'
import StretchOptionsPanel from '@csui/src/PlayerUiPanels/PanelComponents/VideoSettings/StretchOptionsPanel.vue'
import ZoomOptionsPanel from '@csui/src/PlayerUiPanels/PanelComponents/VideoSettings/ZoomOptionsPanel.vue'
export default {
components: {
Button,
CropOptionsPanel,
StretchOptionsPanel
StretchOptionsPanel,
ZoomOptionsPanel,
},
data() {
return {
@ -267,7 +282,7 @@ export default {
gap: 1rem;
> * {
width: calc(50% - 0.5rem);
width: calc(33% - 0.5rem);
}
}

View File

@ -1,9 +0,0 @@
<template>
<div class="flex flex-col">
<div class="">
</div>
</div>
</template>

View File

@ -38,7 +38,7 @@
<!-- CROP OPTIONS -->
<div v-if="settings" class="sub-panel">
<div class="flex flex-row">
<mdicon name="crop" :size="32" />
<mdicon name="crop" :size="16" />
<h1>Crop video:</h1>
</div>

View File

@ -1,7 +1,11 @@
<template>
<div class="button center-text"
:class="{'setting-selected': selected }"
:class="{
'setting-selected': selected,
'no-pad': noPad,
}"
>
<mdicon v-if="icon" :name="icon" :size="iconSize ?? 24"></mdicon>
<div class="label">
{{label}}
</div>
@ -15,6 +19,8 @@ export default {
selected: Boolean,
label: String,
icon: String,
iconSize: Number,
noPad: Boolean
}
}
</script>
@ -40,4 +46,7 @@ export default {
}
}
.no-pad {
padding: 0.5rem 1rem !important;
}
</style>

View File

@ -28,11 +28,20 @@
>
{{ confirmText || 'Confirm' }}
</button>
<button @click="popupVisible = false">{{ cancelText || 'Cancel' }}</button>
<button class="button" @click="popupVisible = false">{{ cancelText || 'Cancel' }}</button>
</div>
</div>
</div>
<button @click="popupVisible = true">
<button
:class="[
{
'danger': dialogType === 'danger',
'warning': dialogType === 'warning',
},
btnClass
]"
@click="popupVisible = true"
>
<slot></slot>
</button>
</template>
@ -46,6 +55,7 @@ export default {
}
},
props: [
'btnClass',
'dialogTitle',
'dialogText',
'confirmText',
@ -54,6 +64,7 @@ export default {
],
methods: {
confirmAction() {
this.popupVisible = false;
this.$emit('onConfirmed');
}
}
@ -67,14 +78,28 @@ export default {
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
backdrop-filter: saturate(50%) backdrop-blur(1rem);
width: 100%;
height: 100%;
// background-color: rgba(0, 0, 0, 0.5);
backdrop-filter: saturate(50%) brightness(50%) blur(1rem);
z-index: 99999;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
.header {
font-size: 1.33rem;
color: #fff;
background-color: #000;
&.danger {
color: rgb(251, 107, 63);
font-weight: bold;
}
}
.body {
min-height: 5rem;

View File

@ -0,0 +1,120 @@
<template>
<div class="flex flex-row w-full">
<button
v-if="editorMode === 'tree'"
@click="() => setEditorMode('text')"
>
Edit as text
</button>
<button
v-else
@click="() => setEditorMode('tree')"
>
Show as tree
</button>
</div>
<div ref="refContainer" class="w-full h-full"></div>
</template>
<script>
import { createJSONEditor, Mode } from "vanilla-jsoneditor";
export default {
props: {
modelValue: Object // v-model binding
},
emits: ["update:modelValue"],
data() {
return {
refContainer: null,
editor: null,
editorMode: Mode.tree,
};
},
watch: {
modelValue: {
deep: true,
handler(newValue) {
if (this.editor) {
this.editor.updateProps({ content: { json: newValue || {} } });
}
}
}
},
mounted() {
if (this.$refs.refContainer) {
this.editor = createJSONEditor({
target: this.$refs.refContainer,
props: {
mode: Mode.tree,
content: { json: this.modelValue || {} },
onChange: (updatedContent) => {
this.$emit("update:modelValue", updatedContent.json);
},
onRenderContextMenu: false,
navigationBar: false,
statusBar: false,
mainMenuBar: false
}
});
}
},
methods: {
setEditorMode(mode) {
this.editorMode = mode
this.editor.updateProps({ mode });
}
},
beforeUnmount() {
if (this.editor) {
this.editor.destroy();
this.editor = null;
}
}
};
</script>
<style lang="scss">
:root {
--jse-panel-background: #111;
--jse-background-color: #000;
--jse-text-color: #ccc;
--jse-key-color: #8c8bef;
--jse-selection-background-color: rgba(255, 171, 102, 0.177);
--jse-context-menu-pointer-background: rgba(255, 171, 102, 0.177);
--jse-delimiter-color: #fa6;
--jse-value-color-boolean: rgb(132, 132, 137);
}
.jse-boolean-toggle[role="checkbox"] {
&[aria-checked="true"] {
color: rgb(218, 244, 238);
}
&[aria-checked="false"] {
border: 0px transparent;
outline: 0px transparent;
color: rgb(241, 107, 25);
background-color: rgb(241, 107, 25);
position: relative;
&:after {
position: absolute;
content: '×';
top: 0;
left: 0;
width: 100%;
height: 100%;
color: black;
text-align: center;
font-weight: bold;
}
}
}
</style>

View File

@ -1,83 +0,0 @@
<template>
<div class="flex flex-col 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>

View File

@ -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>
:&nbsp;
</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>

View File

@ -1,91 +0,0 @@
<template>
<div class="flex flex-col 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>

View File

@ -1,23 +0,0 @@
.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);
}

View File

@ -0,0 +1,105 @@
<template>
<div
v-if="message"
class="popup-overlay"
:class="{'dim': dimOverlay}"
>
<div class="popup-content" >
<div v-if="title" class="header" :class="type">
{{title}}
</div>
<p>
{{ message }}
</p>
<div class="flex flex-row justify-end">
<button
class="primary"
v-if="confirmButtonText"
:class="type"
@click="$emit('onConfirm')"
>
{{ confirmButtonText }}
</button>
<button
:class="type"
@click="$emit('onCancel')"
>
{{ cancelButtonText }}
</button>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
// onConfirm: {
// type: Function,
// required: false
// },
// onCancel: {
// type: Function,
// default: () => this.$emit('close')
// },
title: {
type: String,
required: false
},
message: {
type: String,
required: false, // technically prolly should be true, but we don't have to guard against skill issue
},
dimOverlay: {
type: Boolean,
default: true
},
type: {
type: String,
default: 'info'
},
confirmButtonText: {
type: String,
required: false,
},
cancelButtonText: {
type: String,
default: 'Cancel'
}
}
}
</script>
<style lang="scss" scoped>
.popup-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
// background-color: rgba(0, 0, 0, 0.5);
backdrop-filter: saturate(50%) brightness(50%) blur(1rem);
z-index: 99999;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
.header {
font-size: 1.33rem;
color: #fff;
&.danger {
color: rgb(251, 107, 63);
font-weight: bold;
}
}
.body {
min-height: 5rem;
}
}
</style>

View File

@ -1,44 +1,44 @@
<template>
<div v-if="siteSupportLevel === 'official'" class="site-support official">
<div v-if="siteSupportLevel === 'official'" class="site-support official" :style="supportLevelStyle">
<mdicon name="check-decagram" />
<div v-if="!small">Verified</div>
<div class="tooltip">
<div class="tooltip" :style="tooltipStyle">
<template v-if="small">Verified&nbsp;&nbsp;</template>
The extension is being tested and should work on this site.
</div>
</div>
<div v-if="siteSupportLevel === 'community'" class="site-support community">
<div v-if="siteSupportLevel === 'community'" class="site-support community" :style="supportLevelStyle">
<mdicon name="account-group" />
<div v-if="!small">Community</div>
<div class="tooltip">
<div class="tooltip" :style="tooltipStyle">
<template v-if="small">Community&nbsp;&nbsp;</template>
People say extension works on this site (or have provided help getting the extension to work if it didn't).<br/><br/>
Tamius (the dev) does not test the extension on this site, probably because it requires a subscription or
is geoblocked.
</div>
</div>
<div v-if="siteSupportLevel === 'no-support' || siteSupportLevel === 'unknown'" class="site-support no-support">
<div v-if="siteSupportLevel === 'no-support' || siteSupportLevel === 'unknown'" class="site-support no-support" :style="supportLevelStyle">
<mdicon name="help-circle-outline" />
<div v-if="!small">Unknown</div>
<div class="tooltip">
<div class="tooltip" :style="tooltipStyle">
<template v-if="small">Unknown&nbsp;&nbsp;</template>
Not officially supported. Extension will try to fix things, but no promises.<br/><br/>
Tamius (the dev) does not test the extension on this site for various reasons
(unaware, not using the site, language barrier, geoblocking, paid services Tam doesn't use).
</div>
</div>
<div v-if="siteSupportLevel === 'user-added' || siteSupportLevel === 'user-defined'" class="site-support user-added">
<div v-if="siteSupportLevel === 'user-added' || siteSupportLevel === 'user-defined'" class="site-support user-added" :style="supportLevelStyle">
<mdicon name="account" />
<div v-if="!small">Modified by you</div>
<div class="tooltip">
<div class="tooltip" :style="tooltipStyle">
<template v-if="small">Modified by you&nbsp;&nbsp;</template>
You have manually changed settings for this site. The extension is doing what you told it to do.
</div>
</div>
<div v-if="siteSupportLevel === 'officially-disabled'" class="site-support officially-disabled">
<div v-if="siteSupportLevel === 'officially-disabled'" class="site-support officially-disabled" :style="supportLevelStyle">
<mdicon class="site-support no-support" name="checkbox-marked-circle" />
<div v-if="!small">Not supported</div>
<div class="tooltip">
<div class="tooltip" :style="tooltipStyle">
<template v-if="small">Not supported&nbsp;&nbsp;</template>
Extension is known to not work with this site.
</div>
@ -50,6 +50,8 @@ export default {
props: {
siteSupportLevel: String,
small: Boolean,
supportLevelStyle: String,
tooltipStyle: String,
}
}
</script>

View File

@ -0,0 +1,77 @@
<template>
<button
class="button drop-zone"
:class="[{'drag-over': isDragOver}, className]"
@click="onClick"
@dragover="isDragOver = true"
@dragleave="isDragOver = false"
@drop="onDrop"
>
<slot></slot>
<input type="file" ref="fileInput" @change="onFileChange" accept=".json" />
</button>
</template>
<script>
export default {
props: {
className: {
type: String,
default: ""
}
},
data() {
return {
isDragOver: false
};
},
methods: {
onClick() {
this.$refs.fileInput.click();
},
onDrop(event) {
event.preventDefault();
this.isDragOver = false;
if (event.dataTransfer.files.length > 0) {
this.handleFile(event.dataTransfer.files[0]);
}
},
onFileChange(event) {
if (event.target.files.length > 0) {
this.handleFile(event.target.files[0]);
}
},
handleFile(file) {
if (file.type !== "application/json") {
this.$emit('error', 'NOT_JSON_FILE');
return;
}
const reader = new FileReader();
reader.onload = async (event) => {
try {
const importedData = JSON.parse(event.target.result);
this.$emit('importedJson', importedData);
} catch (error) {
this.$emit('error', 'INVALID_JSON');
}
};
reader.readAsText(file);
},
}
}
</script>
<style lang="scss" scoped>
input {
display: none;
}
.drop-zone {
&.drag-over {
background-color: #fa6;
color: black;
}
}
</style>

View File

@ -1,75 +1,158 @@
<template>
<div class="flex flex-col" style="padding-bottom: 20px">
<div class="flex flex-col relative h-full" style="padding-bottom: 20px">
<!--
Extension is disabled for a given site when it's disabled in full screen, since
current settings do not allow the extension to only be disabled while in full screen
-->
<template v-if="siteSettings.isEnabledForEnvironment(false, true) === ExtensionMode.Disabled">
<template v-if="siteSettings.isEnabledForEnvironment(false, true) === ExtensionMode.Disabled && !enabledHosts?.length">
<div class="h-full flex flex-col items-center justify-center" style="margin-top: 8rem">
<div class="info">
Extension is not enabled for this site.
</div>
</template>
<div class="flex flex-row">
<mdicon name="crop" :size="24" />&nbsp;&nbsp;
<h1>Crop video:</h1>
<div>
Please enable extension for this site.
</div>
<div>
<button
class="flex flex-row items-center"
style="background-color: transparent; padding: 0.25rem 0.5rem; margin-top: 1rem;"
@click="openSettings()"
>
Open settings <mdicon style="margin-left: 0.5rem;" name="open-in-new" size="16"></mdicon>
</button>
</div>
</div>
</template>
<template v-else>
<div
v-if="siteSettings.isEnabledForEnvironment(false, true) === ExtensionMode.Disabled"
class="warning-compact"
>
<div class="w-full flex flex-row">
<div class="grow">
<b>Extension is disabled for this site.</b>
</div>
<div>
<button
class="flex flex-row items-center"
style="border: 1px solid black; background-color: transparent; color: black; padding: 0.25rem 0.5rem; margin-top: -0.25rem; margin-right: -0.5rem;"
@click="openSettings()"
>
Open settings <mdicon style="margin-left: 0.5rem;" name="open-in-new" size="16"></mdicon>
</button>
</div>
</div>
<small>Controls will only work on content embedded from the following sites:</small><br/>
<div class="w-full flex flex-row justify-center">
<span v-for="host of enabledHosts" :key="host" class="website-name">{{host}}</span>
</div>
</div>
<div class="flex flex-row">
<mdicon name="crop" :size="16" />&nbsp;&nbsp;
<span>CROP</span>
</div>
<div
style="margin-top: -0.69rem; margin-bottom: 0.88rem;"
>
<CropOptionsPanel
style="margin-top: -2rem"
:settings="settings"
:eventBus="eventBus"
:siteSettings="siteSettings"
:isEditing="false"
:compact="true"
>
</CropOptionsPanel>
<div class="flex flex-row">
<mdicon name="crop" :size="24" />&nbsp;&nbsp;
<h1>Stretch video:</h1>
</div>
<div class="flex flex-row">
<mdicon name="crop" :size="16" />&nbsp;&nbsp;
<span>STRETCH</span>
</div>
<div
style="margin-top: -0.69rem; margin-bottom: 0.88rem;"
>
<StretchOptionsPanel
style="margin-top: -2rem"
:settings="settings"
:eventBus="eventBus"
:siteSettings="siteSettings"
:isEditing="false"
:compact="true"
></StretchOptionsPanel>
</div>
<div class="flex flex-row">
<mdicon name="crop" :size="24" />&nbsp;&nbsp;
<h1>Zoom:</h1>
<mdicon name="crop" :size="16" />&nbsp;&nbsp;
<span>ZOOM</span>
</div>
<div
style="margin-top: -0.69rem; margin-bottom: 0.88rem;"
>
<ZoomOptionsPanel
:settings="settings"
:eventBus="eventBus"
:siteSettings="siteSettings"
:isEditing="false"
:compact="true"
>
</ZoomOptionsPanel>
</div>
<div class="flex flex-row">
<mdicon name="crop" :size="16" />&nbsp;&nbsp;
<span>ALIGN</span>
</div>
<div
style="margin-bottom: 0.88rem;"
>
<AlignmentOptionsControlComponent
:eventBus="eventBus"
:large="true"
> </AlignmentOptionsControlComponent>
</div>
</template>
</div>
</template>
<script>
import CropOptionsPanel from '../../PlayerUiPanels/PanelComponents/VideoSettings/CropOptionsPanel';
import StretchOptionsPanel from '../../PlayerUiPanels/PanelComponents/VideoSettings/StretchOptionsPanel.vue';
import ZoomOptionsPanel from '../../PlayerUiPanels/PanelComponents/VideoSettings/ZoomOptionsPanel.vue';
import CropOptionsPanel from '@csui/src/PlayerUiPanels/PanelComponents/VideoSettings/CropOptionsPanel';
import StretchOptionsPanel from '@csui/src/PlayerUiPanels/PanelComponents/VideoSettings/StretchOptionsPanel.vue';
import ZoomOptionsPanel from '@csui/src/PlayerUiPanels/PanelComponents/VideoSettings/ZoomOptionsPanel.vue';
import ExtensionMode from '@src/common/enums/ExtensionMode.enum.ts';
import AlignmentOptionsControlComponent from '@csui/src/PlayerUiPanels/AlignmentOptionsControlComponent.vue';
import { SiteSettings } from '../../../../ext/lib/settings/SiteSettings';
export default {
data() {
return {
exec: null,
ExtensionMode: ExtensionMode,
};
components: {
CropOptionsPanel,
StretchOptionsPanel,
ZoomOptionsPanel,
AlignmentOptionsControlComponent
},
mixins: [
],
props: [
'site',
'settings',
'siteSettings',
'eventBus',
'hosts'
],
components: {
CropOptionsPanel, StretchOptionsPanel, ZoomOptionsPanel
data() {
return {
exec: null,
ExtensionMode: ExtensionMode,
enabledHosts: [],
};
},
watch: {
hosts(val) {
this.filterActiveSites(val);
}
},
created() {
this.eventBus.subscribe(
@ -79,6 +162,7 @@ export default {
function: (config) => this.handleConfigBroadcast(config)
}
);
this.filterActiveSites(this.hosts);
},
mounted() {
this.eventBus.sendToTunnel('get-ar');
@ -87,8 +171,37 @@ export default {
this.eventBus.unsubscribeAll(this);
},
methods: {
filterActiveSites(val) {
this.enabledHosts = [];
for (const host of val) {
const siteSettings = new SiteSettings(this.settings, host);
if (siteSettings.isEnabledForEnvironment(false, true) === ExtensionMode.Enabled) {
this.enabledHosts.push(host);
}
}
},
openSettings() {
this.eventBus.send('open-popup-settings', {tab: 'extensionSettings'})
}
}
}
</script>
<style lang="scss" scoped>
.warning-compact {
background-color: #d6ba4a;
color: #000;
padding: 0.5rem 1rem;
margin-top: -0.5rem;
margin-bottom: 0.5rem;
.website-name {
font-size: 0.85rem;
&:not(:last-of-type)::after {
content: ','
}
}
}
</style>

View File

@ -1,58 +0,0 @@
<template>
<div>
<h2>What's new</h2>
<p>Full changelog for older versions <a href="https://github.com/tamius-han/ultrawidify/blob/master/CHANGELOG.md">is available here</a>.</p>
<p class="label">6.0.0</p>
<ul>
<li>Fixed laginess in Chromium-based browsers on Windows. Details in <a href="https://github.com/tamius-han/ultrawidify/issues/199#issuecomment-1221383134" target="blank">#199</a>.</li>
</ul>
<p class="label">5.1.2</p>
<ul>
<li>
<b>In-player UI.</b><br/>
This one took major effort to pull off. Required some changes under the hood, required me to de-spaghettify some code. The UI will only show
if the video takes up sufficient amount of space (currently determined as 960 pixels wide).
</li>
<li>
By default, Ultrawidify will not kick in on "small videos". You have to be either in full screen or theater mode. Ultrawidify assumes that any
video that takes up more than 85% of the window width is being viewed in theater mode. This value may be tweaked later on. This feature can be
configured (or turned off entirely) via 'Advanced options' menu on the in-player UI.
</li>
<li>
The in-player extension UI will do a better job differentiating between the various level of support: "official" for the sites that Tam can check
on his own, "fingers crossed" for sites that Tam can't check because my load-bearing credit card can't support the weight of a dozen subscription
services, and "community support" for sites that enjoy support through the helping hand of people willing to help.
</li>
<li>
The in-player UI now displays a warning whenever Ultrawidify detects automatic aspect ratio detection isn't happening because the site uses DRM.
</li>
<li>
Made the UI for fixing problems with Ultrawidify not working a bit more intuitive.
</li>
<li>
Better zooming. The slider is back, baby.
</li>
<li>
New alignment options. Video can be aligned vertically as well as horizontally.
</li>
<li>
Panning option that's a bit more intuitive.
</li>
</ul>
</div>
</template>
<script>
import BrowserDetect from '../../ext/conf/BrowserDetect';
export default {
data () {
return {
BrowserDetect: BrowserDetect
}
},
}
</script>
<style>
</style>

View File

@ -1,42 +1,109 @@
<template>
<div>
<div class="flex flex-row" style="width: 250px;">
<div class="flex-grow">
Custom zoom
</div>
<div class="flex flex-row">
<Button
v-if="zoomAspectRatioLocked"
icon="lock"
:iconSize="16"
:fixedWidth="true"
:noPad="true"
@click="toggleZoomAr()"
>
</Button>
<Button
v-else
icon="lock-open-variant"
:iconSize="16"
:fixedWidth="true"
:noPad="true"
@click="toggleZoomAr()"
>
</Button>
<Button
icon="restore"
:iconSize="16"
:noPad="true"
@click="resetZoom()"
></Button>
</div>
</div>
<div class="top-label">Zoom:</div>
<div class="input range-input">
<template v-if="zoomAspectRatioLocked">
<div class="input range-input no-bg">
<input
type="range"
class="slider"
min="0"
min="-1"
max="3"
step="0.01"
:value="zoom.x"
@input="changeZoom($event.target.value)"
/>
<input
class="disabled"
style="width: 2rem;"
:value="getZoomForDisplay('x')"
/>
</div>
<template v-if="true">
<div class="top-label">Vertical zoom:</div>
<div class="input range-input">
</template>
<template v-else>
<div class="top-label">Horizontal zoom:</div>
<div class="input range-input no-bg">
<input
type="range"
class="slider"
min="0"
min="-1"
max="3"
step="0.01"
:value="zoom.x"
@input="changeZoom($event.target.value, 'x')"
/>
<input
class="disabled"
style="width: 2rem;"
:value="getZoomForDisplay('x')"
/>
</div>
<div class="top-label">Vertical zoom:</div>
<div class="input range-input no-bg">
<input
type="range"
class="slider"
min="-1"
max="3"
step="0.01"
:value="zoom.y"
@input="changeZoom($event.target.value, 'y')"
/>
<input
class="disabled"
style="width: 2rem;"
:value="getZoomForDisplay('y')"
/>
</div>
</template>
<div><input type="checkbox"/> Control vertical and horizontal zoom independently.</div>
</template>
<script>
import Button from '@csui/src/components/Button.vue';
import * as _ from 'lodash';
export default {
components: {
Button,
},
mixins: [
],
props: [
'settings', // required for buttons and actions, which are global
'eventBus'
],
data() {
return {
zoomAspectRatioLocked: true,
@ -51,17 +118,42 @@ export default {
stretch: null,
zoom: null,
pan: null
},
pollingInterval: undefined,
debouncedGetEffectiveZoom: undefined,
}
},
created() {
this.eventBus?.subscribeMulti(
{
'announce-zoom': {
function: (data) => {
this.zoom = {
x: Math.log2(data.x),
y: Math.log2(data.y)
};
}
}
},
mixins: [
],
props: [
'settings', // required for buttons and actions, which are global
'eventBus'
],
this
);
this.debouncedGetEffectiveZoom = _.debounce(
() => {
this.getEffectiveZoom();
},
250
),
this.getEffectiveZoom();
this.pollingInterval = setInterval(this.debouncedGetEffectiveZoom, 2000);
},
destroyed() {
this.eventBus.unsubscribe(this);
clearInterval(this.pollingInterval);
},
methods: {
getEffectiveZoom() {
this.eventBus?.sendToTunnel('get-effective-zoom', {});
},
getZoomForDisplay(axis) {
// zoom is internally handled logarithmically, because we want to have x0.5, x1, x2, x4 ... magnifications
// spaced out at regular intervals. When displaying, we need to convert that to non-logarithmic values.
@ -81,25 +173,36 @@ export default {
// this.eventBus.send('set-zoom', {zoom: 1, axis: 'y'});
// this.eventBus.send('set-zoom', {zoom: 1, axis: 'x'});
this.eventBus?.sendToTunnel('set-zoom', {zoom: 1, axis: 'y'});
this.eventBus?.sendToTunnel('set-zoom', {zoom: 1, axis: 'x'});
this.eventBus?.sendToTunnel('set-zoom', {zoom: 1});
},
changeZoom(newZoom, axis) {
// we store zoom logarithmically on this compnent
if (!axis) {
this.zoom.x = newZoom;
changeZoom(newZoom, axis, isLinear) {
if (isNaN(+newZoom)) {
return;
}
let logZoom, linZoom;
if (isLinear) {
newZoom /= 100;
logZoom = Math.log2(newZoom);
linZoom = newZoom;
} else {
this.zoom[axis] = newZoom;
logZoom = newZoom;
linZoom = Math.pow(2, newZoom);
}
// we store zoom logarithmically on this component
if (!axis) {
this.zoom.x = logZoom;
} else {
this.zoom[axis] = logZoom;
}
// we do not use logarithmic zoom elsewhere, therefore we need to convert
newZoom = Math.pow(2, newZoom);
if (this.zoomAspectRatioLocked) {
this.eventBus?.sendToTunnel('set-zoom', {zoom: newZoom, axis: 'y'});
this.eventBus?.sendToTunnel('set-zoom', {zoom: newZoom, axis: 'x'});
this.eventBus?.sendToTunnel('set-zoom', {zoom: linZoom});
} else {
this.eventBus?.sendToTunnel('set-zoom', {zoom: newZoom, axis: axis ?? 'x'});
this.eventBus?.sendToTunnel('set-zoom', {zoom: {[axis ?? 'x']: linZoom}});
}
},
}

View File

@ -58,10 +58,19 @@ button, .button {
border-color: rgba($primary, .5);
}
&.primary {
background-color: #fa6;
color: #000;
}
&.danger {
background-color: #ff2211 !important;
color:#000;
}
&.disabled {
filter: saturate(0%);
}
}
.b3 {
margin: 0.25rem;
@ -80,6 +89,12 @@ button, .button {
border-bottom: 1px solid rgba($primary, 0.5);
}
&.no-bg {
background-color: transparent;
border-color: transparent;
}
input {
width: 100%;
outline: none;
@ -154,6 +169,7 @@ button, .button {
flex-grow: 1;
flex-shrink: 1;
max-width: 24rem;
min-width: 16rem;
}
.has-hint {
@ -165,8 +181,15 @@ button, .button {
flex-grow: 1;
flex-shrink: 1;
min-width: 12px;
max-width: 24rem;
select {
background: rgba($blackBg, $hoverTransparentOpacity);
min-width: 12px;
max-width: 100%;
color: #fff;
// border: 0px solid transparent;
border: 1px solid rgba(255, 171, 102, 0.42);
@ -208,9 +231,15 @@ button, .button {
padding-left: 0.33rem;
padding-right: 0.33rem;
}
.input-slider {
width: 480px;
.b3-compact {
width: 7rem;
padding-left: 0.25rem;
padding-right: 0.25rem;
}
// .input-slider {
// width: 480px;
// }
.warning-lite {
padding-right: 16px;
padding-bottom: 16px;

View File

@ -1,15 +1,13 @@
import Debug from './conf/Debug';
import ExtensionMode from '../common/enums/ExtensionMode.enum';
import Settings from './lib/Settings';
import Comms from './lib/comms/Comms';
import CommsClient from './lib/comms/CommsClient';
import PageInfo from './lib/video-data/PageInfo';
import Logger, { baseLoggingOptions } from './lib/Logger';
import UWGlobals from './lib/UWGlobals';
import EventBus from './lib/EventBus';
import KeyboardHandler from './lib/kbm/KeyboardHandler';
import { SiteSettings } from './lib/settings/SiteSettings';
import UI from './lib/uwui/UI';
import { BLANK_LOGGER_CONFIG, LogAggregator } from './lib/logging/LogAggregator';
import { ComponentLogger } from './lib/logging/ComponentLogger';
export default class UWContent {
pageInfo: PageInfo;
@ -17,7 +15,8 @@ export default class UWContent {
settings: Settings;
siteSettings: SiteSettings;
keyboardHandler: KeyboardHandler;
logger: Logger;
logAggregator: LogAggregator;
logger: ComponentLogger;
eventBus: EventBus;
isIframe: boolean = false;
@ -34,7 +33,7 @@ export default class UWContent {
reloadSettings() {
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();
} catch (e) {
console.warn('Ultrawidify: settings reload failed. This probably shouldn\'t outright kill the extension, but page reload is recommended.');
@ -50,8 +49,9 @@ export default class UWContent {
// logger init is the first thing that needs to run
try {
if (!this.logger) {
this.logger = new Logger();
await this.logger.init(baseLoggingOptions);
this.logAggregator = new LogAggregator('◈');
this.logger = new ComponentLogger(this.logAggregator, 'UWContent');
await this.logAggregator.init(BLANK_LOGGER_CONFIG);
}
} catch (e) {
console.error("logger init failed!", e)
@ -67,7 +67,7 @@ export default class UWContent {
if (!this.settings) {
this.settings = new Settings({
onSettingsChanged: () => this.reloadSettings(),
logger: this.logger
logAggregator: this.logAggregator
});
await this.settings.init();
this.siteSettings = this.settings.getSiteSettings();
@ -81,13 +81,13 @@ export default class UWContent {
function: () => this.initPhase2()
}
);
this.comms = new CommsClient('content-main-port', this.logger, this.eventBus);
this.comms = new CommsClient('content-main-port', this.logAggregator, this.eventBus);
this.eventBus.setComms(this.comms);
this.initPhase2();
} catch (e) {
console.error('Ultrawidify initalization failed for some reason:', e);
console.error('Ultrawidify initialization failed for some reason:', e);
}
}
@ -95,21 +95,21 @@ export default class UWContent {
initPhase2() {
try {
if (this.pageInfo) {
this.logger.log('info', 'debug', '[uw.js::setup] An instance of pageInfo already exists and will be destroyed.');
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.logger);
this.logger.log('info', 'debug', "[uw.js::setup] pageInfo initialized.");
this.pageInfo = new PageInfo(this.eventBus, this.siteSettings, this.settings, this.logAggregator);
this.logger.debug('setup', "pageInfo initialized.");
this.logger.log('info', 'debug', "[uw.js::setup] will try to initate KeyboardHandler.");
this.logger.debug('setup', "will try to initate KeyboardHandler.");
if (this.keyboardHandler) {
this.keyboardHandler.destroy();
}
this.keyboardHandler = new KeyboardHandler(this.eventBus, this.siteSettings, this.settings, this.logger);
this.keyboardHandler = new KeyboardHandler(this.eventBus, this.siteSettings, this.settings, this.logAggregator);
this.keyboardHandler.init();
this.logger.log('info', 'debug', "[uw.js::setup] KeyboardHandler initiated.");
this.logger.debug('setup', "KeyboardHandler initiated.");
this.globalUi = new UI('ultrawidify-global-ui', {eventBus: this.eventBus, isGlobal: true});
this.globalUi.enable();
@ -117,7 +117,7 @@ export default class UWContent {
} catch (e) {
console.error('Ultrawidify: failed to start extension. Error:', e)
this.logger.log('error', 'debug', "[uw::init] FAILED TO START EXTENSION. Error:", e);
this.logger.error('setup', "FAILED TO START EXTENSION. Error:", e);
}
}

View File

@ -2,13 +2,20 @@ import Debug from './conf/Debug.js';
import BrowserDetect from './conf/BrowserDetect';
import CommsServer from './lib/comms/CommsServer';
import Settings from './lib/Settings';
import Logger, { baseLoggingOptions } from './lib/Logger';
import { sleep } from '../common/js/utils';
import EventBus, { EventBusCommand } from './lib/EventBus';
import { ComponentLogger } from './lib/logging/ComponentLogger';
import { BLANK_LOGGER_CONFIG, LogAggregator } from './lib/logging/LogAggregator';
const BASE_LOGGING_STYLES = {
'log': 'background-color: #243; color: #4a8',
}
export default class UWServer {
settings: Settings;
logger: Logger;
logger: ComponentLogger;
logAggregator: LogAggregator;
comms: CommsServer;
eventBus: EventBus;
@ -64,22 +71,13 @@ export default class UWServer {
async setup() {
try {
// logger is the first thing that goes up
const loggingOptions = {
isBackgroundScript: true,
allowLogging: false,
useConfFromStorage: true,
logAll: true,
fileOptions: {
enabled: false,
},
consoleOptions: {
enabled: false
}
};
this.logger = new Logger();
await this.logger.init(loggingOptions);
const loggingOptions = BLANK_LOGGER_CONFIG;
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();
this.eventBus = new EventBus({isUWServer: true});
@ -95,12 +93,16 @@ export default class UWServer {
}
}
async _promisifyTabsGet(browserObj, tabId){
return new Promise( (resolve, reject) => {
browserObj.tabs.get(tabId, (tab) => resolve(tab));
});
}
//#region CSS managemeent
async injectCss(css, sender) {
if (!css) {
return;
@ -130,7 +132,7 @@ export default class UWServer {
});
}
} 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) {
@ -160,16 +162,16 @@ export default class UWServer {
});
}
} catch (e) {
this.logger.log('error','debug', '[UwServer::injectCss] Error while removing css:', {error: e, css, sender});
this.logger.error('injectCss', 'Error while removing css:', {error: e, css, sender});
}
}
async replaceCss(oldCss, newCss, sender) {
if (oldCss !== newCss) {
this.removeCss(oldCss, sender);
this.injectCss(newCss, sender);
}
}
//#endregion
extractHostname(url){
var hostname;
@ -206,20 +208,21 @@ export default class UWServer {
}
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) {
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 = {
'siteSettings': undefined,
'videoSettings': undefined,
}
//TODO: change extension icon based on whether there's any videos on current page
}
registerVideo(sender) {
this.logger.log('info', 'comms', '[UWServer::registerVideo] Registering video.\nsender:', sender);
this.logger.info('registerVideo', 'Registering video.\nsender:', sender);
const tabHostname = this.extractHostname(sender.tab.url);
const frameHostname = this.extractHostname(sender.url);
@ -253,11 +256,11 @@ 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) {
this.logger.log('info', 'comms', '[UwServer::unregisterVideo] Unregistering video.\nsender:', sender);
this.logger.info('unregisterVideo', 'Unregistering video.\nsender:', sender);
if (this.videoTabs[sender.tab.id]) {
if ( Object.keys(this.videoTabs[sender.tab.id].frames).length <= 1) {
delete this.videoTabs[sender.tab.id]
@ -267,20 +270,33 @@ 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) {
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;
}
async getCurrentSite() {
this.logger.info('getCurrentSite', 'received get-current-site ...');
const site = await this.getVideoTab();
// Don't propagate 'INVALID SITE' to the popup.
if (site.host === 'INVALID SITE') {
this.logger.info('getCurrentSite', 'Host is not valid — no info for current tab.');
return;
}
const tabHostname = await this.getCurrentTabHostname();
this.logger.info('getCurrentSite', 'Returning data:', {site, tabHostname});
this.eventBus.send(
'set-current-site',
{
site: await this.getVideoTab(),
tabHostname: await this.getCurrentTabHostname(),
site,
tabHostname,
},
{
comms: {
@ -305,9 +321,12 @@ export default class UWServer {
return {
host: 'INVALID SITE',
frames: [],
hostnames: [],
}
}
const hostnames = await this.comms.listUniqueFrameHosts();
if (this.videoTabs[ctab.id]) {
// if video is older than PageInfo's video rescan period (+ 4000ms of grace),
// we clean it up from videoTabs[tabId].frames array.
@ -330,6 +349,7 @@ export default class UWServer {
return {
...this.videoTabs[ctab.id],
host: this.extractHostname(ctab.url),
hostnames,
selected: this.selectedSubitem
};
}
@ -339,7 +359,8 @@ export default class UWServer {
return {
host: this.extractHostname(ctab.url),
frames: [],
selected: this.selectedSubitem
hostnames,
selected: this.selectedSubitem,
}
}
@ -347,12 +368,15 @@ export default class UWServer {
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);
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

View File

@ -8,201 +8,10 @@ import SettingsInterface from '../../common/interfaces/SettingsInterface';
import { _cp } from '../../common/js/utils';
import CropModePersistence from '../../common/enums/CropModePersistence.enum';
import AspectRatioType from '../../common/enums/AspectRatioType.enum';
import { update } from 'lodash';
const ExtensionConfPatch = [
const ExtensionConfPatch = Object.freeze([
{
forVersion: '6.1.1',
updateFn: (userOptions: SettingsInterface, defaultOptions) => {
// add new commands
userOptions.commands = defaultOptions.commands;
userOptions.actions = defaultOptions.actions;
}
}, {
// NOTE - when releasing shit, ensure ALL alpha migrations are combined together in one function
forVersion: '6.1.2',
updateFn: (userOptions, defaultOptions) => {
userOptions.commands = defaultOptions.commands;
// migrates old settings regarding whether extension is enabled or not
const copyEnabled = (site) => {
userOptions.sites[site].enable = {
fullscreen: userOptions.sites[site].mode,
theater: userOptions.sites[site].mode,
normal: ExtensionMode.Disabled
};
userOptions.sites[site].enableKeyboard = {
fullscreen: userOptions.sites[site].keyboardShortcutsEnabled,
theater: userOptions.sites[site].keyboardShortcutsEnabled,
normal: ExtensionMode.Disabled
};
userOptions.sites[site].enableAard = {
fullscreen: userOptions.sites[site].autoar,
theater: userOptions.sites[site].autoar,
normal: ExtensionMode.Disabled
};
userOptions.sites[site].stretchModePersistence = userOptions.sites[site].cropModePersistence;
// remove old options
delete userOptions.sites[site].mode;
delete userOptions.sites[site].keyboardShortcutsEnabled;
delete userOptions.sites[site].autoar;
}
// globals get carried over before other sites:
copyEnabled('@global');
// we make another guess about a new option we just added
for (const key in userOptions.sites) {
// we already had this
if (key === '@global') {
continue;
}
copyEnabled(key);
userOptions.sites[key].DOMConfig = _cp(defaultOptions.sites[key].DOMConfig)
// convert old site.DOM to site.DOMConfig[]
if (userOptions.sites[key].type === 'user-defined') {
const DOM = userOptions.sites[key].DOM;
if (DOM) {
userOptions.sites[key].DOMConfig['user-defined'] = {
type: 'user-1',
customCss: DOM?.css,
periodicallyRefreshPlayerElement: DOM?.player?.periodicallyRefreshPlayerElement,
elements: !(DOM?.player) ? undefined : {
player: {
manual: DOM?.player?.manual,
querySelectors: DOM?.player?.useRelativeAncestor ? undefined : DOM?.player?.querySelectors,
index: DOM?.player?.useRelativeAncestor ? DOM?.player?.videoAncestor : undefined,
}
}
}
userOptions.sites[key].activeDOMConfig = 'user-1';
// remove old configuration
delete userOptions.sites[key].DOM;
}
}
}
}
}, {
forVersion: '6.0.3',
updateFn: (userOptions: SettingsInterface, defaultOptions) => {
delete (userOptions as any).sites['@global'].persistOption;
delete (userOptions as any).sites['@empty'].persistOption;
userOptions.sites['@global'].persistCSA = CropModePersistence.Disabled;
userOptions.sites['@empty'].persistCSA = CropModePersistence.Disabled;
}
}, {
forVersion: '6.0.4',
updateFn: (userOptions: SettingsInterface, defaultOptions) => {
// deprecated much?
userOptions.actions.push({
name: 'Cycle aspect ratio',
label: 'Cycle',
cmd: [{
action: 'set-ar',
arg: AspectRatioType.Cycle
}]
});
// userOptions.commands.crop.push({
// action: 'set-ar',
// label: 'Cycle',
// comment: 'Cycle through crop options',
// arguments: {
// type: AspectRatioType.Cycle
// },
// shortcut: {
// key: 'c',
// code: 'KeyC',
// ctrlKey: false,
// metaKey: false,
// altKey: false,
// shiftKey: false,
// onKeyUp: true,
// onKeyDown: false,
// }
// });
// userOptions.commands.crop.push({
// action: 'set-ar',
// label: '32:9',
// comment: 'Crop for 32:9 aspect ratio',
// arguments: {
// type: AspectRatioType.Fixed,
// ratio: 3.56
// },
// })
}
}, {
forVersion: '6.1.5',
updateFn: (userOptions: SettingsInterface, defaultOptions) => {
if (!userOptions.sites['@global'].defaults.alignment || !userOptions.sites['@global'].defaults.alignment.x || !userOptions.sites['@global'].defaults.alignment.y) {
userOptions.sites['@global'].defaults.alignment = {
x: VideoAlignmentType.Center,
y: VideoAlignmentType.Center
};
}
userOptions.sites['@empty'].defaults.alignment = {x: VideoAlignmentType.Default, y: VideoAlignmentType.Default};
}
}, {
forVersion: '6.1.1-6',
updateFn: (userOptions: SettingsInterface, defaultOptions) => {
for (const site in userOptions.sites) {
userOptions.sites[site].defaultType = userOptions.sites[site].type as any;
}
userOptions.sites['@global'].defaultType = 'unknown';
userOptions.sites['@empty'].defaultType = 'modified';
}
}, {
forVersion: '6.1.2-0',
updateFn: (userOptions: SettingsInterface, defaultOptions) => {
// remove custom CSS, as it is no longer needed
for (const site in userOptions.sites) {
for (const domOption in userOptions.sites[site].DOMConfig)
userOptions.sites[site].DOMConfig[domOption].customCss;
}
userOptions.arDetect.aardType = 'auto';
userOptions.ui = {
inPlayer: {
enabled: true, // enable by default on new installs
enabledFullscreenOnly: false,
minEnabledWidth: 0.75,
minEnabledHeight: 0.75,
activation: 'player',
popupAlignment: 'left',
triggerZoneDimensions: {
width: 0.5,
height: 0.5,
offsetX: -50,
offsetY: 0,
}
}
},
userOptions.newFeatureTracker['uw6.ui-popup'] = {show: 10};
}
}, {
forVersion: '6.2.1',
updateFn: (userOptions: SettingsInterface, defaultOptions) => {
userOptions.ui = defaultOptions.ui;
userOptions.arDetect = defaultOptions.arDetect;
userOptions.newFeatureTracker = defaultOptions.newFeatureTracker;
}
}, {
forVersion: '6.2.3',
updateFn: (userOptions: SettingsInterface, defaultOptions) => {
for (const site in userOptions.sites) {
if (userOptions.sites[site].defaults?.stretch && !userOptions.sites[site].defaults?.stretch.type) {
userOptions.sites[site].defaults.stretch = {type: userOptions.sites[site].defaults?.stretch as any as StretchType};
}
}
}
}, {
forVersion: '6.2.4',
updateFn: (userOptions: SettingsInterface, defaultOptions) => {
for (const site in userOptions.sites) {
@ -212,7 +21,6 @@ const ExtensionConfPatch = [
normal: ExtensionMode.Default,
}
}
const uiEnabled =
userOptions.sites['@global'].enableUI = {
fullscreen: userOptions.ui.inPlayer.enabled ? ExtensionMode.Enabled : ExtensionMode.Disabled,
theater: ExtensionMode.Enabled,
@ -224,8 +32,215 @@ const ExtensionConfPatch = [
normal: ExtensionMode.Default,
}
}
}, {
forVersion: '6.2.6',
updateFn: (userOptions: SettingsInterface, defaultOptions) => {
console.warn('[ultrawidify] STARTING SETTINGS MIGRATION TO 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
});
}
}
}
}
]);
export default ExtensionConfPatch;

View File

@ -14,6 +14,10 @@ if(Debug.debug)
console.log("Loading: ExtensionConf.js");
const ExtensionConf: SettingsInterface = {
dev: {
loadFromSnapshot: false,
},
arDetect: {
aardType: 'auto',
@ -127,6 +131,12 @@ const ExtensionConf: SettingsInterface = {
offsetX: -50,
offsetY: 0
}
},
dev: {
aardDebugOverlay: {
showOnStartup: false,
showDetectionDetails: true
}
}
},
@ -354,9 +364,9 @@ const ExtensionConf: SettingsInterface = {
}],
zoom: [{
action: 'change-zoom',
label: 'Zoom +10%',
label: 'Zoom +5%',
arguments: {
zoom: 0.1
zoom: 0.05
},
shortcut: {
key: 'z',
@ -372,9 +382,9 @@ const ExtensionConf: SettingsInterface = {
actionId: 'change-zoom-10in'
}, {
action: 'change-zoom',
label: 'Zoom -10%',
label: 'Zoom -5%',
arguments: {
zoom: -0.1
zoom: -0.05
},
shortcut: {
key: 'u',
@ -391,11 +401,99 @@ const ExtensionConf: SettingsInterface = {
}, {
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',
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 crop 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: 'Crop 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: 'Crop 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: 'Crop for 32:9 aspect ratio',
arguments: {
type: AspectRatioType.Fixed,
ratio: 3.56
},
}],
pan: [{
action: 'set-alignment',
@ -577,803 +675,6 @@ const ExtensionConf: SettingsInterface = {
internalOnly: true
}]
},
// -----------------------------------------
// ::: ACTIONS :::
// -----------------------------------------
actions: [{
name: 'Trigger automatic detection', // name displayed in settings
label: 'Automatic', // name displayed in ui (can be overridden in scope/playerUi)
cmd: [{
action: 'set-ar',
arg: AspectRatioType.Automatic,
persistent: false, // optional, false by default. If true, change doesn't take effect immediately.
// Instead, this action saves stuff to settings
}],
scopes: {
global: { // if 'global' is undefined, 'show' is presumed to be 'false'
show: false,
},
site: {
show: false,
},
page: {
show: true,
label: 'Automatic', // example override, takes precedence over default label
shortcut: [{
key: 'a',
code: 'KeyA',
ctrlKey: false,
metaKey: false,
altKey: false,
shiftKey: false,
onKeyUp: true,
onKeyDown: false,
}],
}
},
playerUi: {
show: true,
path: 'crop',
},
}, {
name: 'Reset to default',
label: 'Reset',
cmd: [{
action: 'set-ar',
arg: AspectRatioType.Reset,
}],
scopes: {
page: {
show: true,
shortcut: [{
key: 'r',
code: 'KeyR',
ctrlKey: false,
metaKey: false,
altKey: false,
shiftKey: false,
onKeyUp: true,
onKeyDown: false,
}],
}
},
playerUi: {
show: true,
path: 'crop'
},
}, {
name: 'Fit to width',
label: 'Fit width',
cmd: [{
action: 'set-ar',
arg: AspectRatioType.FitWidth,
}],
scopes: {
page: {
show: true,
shortcut: [{
key: 'w',
code: 'KeyW',
ctrlKey: false,
metaKey: false,
altKey: false,
shiftKey: false,
onKeyUp: true,
onKeyDown: false,
}],
}
},
playerUi: {
show: true,
path: 'crop'
}
}, {
name: 'Fit to height',
label: 'Fit height',
cmd: [{
action: 'set-ar',
arg: AspectRatioType.FitHeight
}],
scopes: {
page: {
show: true,
shortcut: [{
key: 'e',
code: 'KeyE',
ctrlKey: false,
metaKey: false,
altKey: false,
shiftKey: false,
onKeyUp: true,
onKeyDown: false,
}]
}
},
playerUi: {
show: true,
path: 'crop'
}
}, {
name: 'Cycle aspect ratio',
label: 'Cycle',
cmd: [{
action: 'set-ar',
arg: AspectRatioType.Cycle
}]
},{
userAdded: true,
name: 'Set aspect ratio to 16:9',
label: '16:9',
cmd: [{
action: 'set-ar',
arg: AspectRatioType.Fixed,
customArg: 1.78,
}],
scopes: {
page: {
show: true,
shortcut: [{
key: 's',
code: 'KeyS',
ctrlKey: false,
metaKey: false,
altKey: false,
shiftKey: false,
onKeyUp: false,
onKeyDown: true,
}],
}
},
playerUi: {
show: true,
path: 'crop'
}
}, {
userAdded: true,
name: 'Set aspect ratio to 21:9 (2.39:1)',
label: '21:9',
cmd: [{
action: 'set-ar',
arg: AspectRatioType.Fixed,
customArg: 2.39
}],
scopes: {
page: {
show: true,
shortcut: [{
key: 'd',
code: 'KeyD',
ctrlKey: false,
metaKey: false,
altKey: false,
shiftKey: false,
onKeyUp: false,
onKeyDown: true,
}]
}
},
playerUi: {
show: true,
path: 'crop'
}
}, {
userAdded: true,
name: 'Set aspect ratio to 18:9',
label: '18:9',
cmd: [{
action: 'set-ar',
arg: AspectRatioType.Fixed,
customArg: 2.0,
}],
scopes: {
page: {
show: true,
shortcut: [{
key: 'x',
code: 'KeyX',
ctrlKey: false,
metaKey: false,
altKey: false,
shiftKey: false,
onKeyUp: true,
onKeyDown: false,
}]
}
},
playerUi: {
show: true,
path: 'crop',
}
}, {
name: 'Don\'t persist crop',
label: 'Never persist',
cmd: [{
action: 'set-ar-persistence',
arg: CropModePersistence.Disabled,
}],
scopes: {
site: {
show: true,
},
global: {
show: true,
}
},
playerUi: {
show: true,
}
}, {
name: 'Persist crop while on page',
label: 'Until page load',
cmd: [{
action: 'set-ar-persistence',
arg: CropModePersistence.UntilPageReload,
}],
scopes: {
site: {
show: true,
},
global: {
show: true,
}
},
playerUi: {
show: true,
}
}, {
name: 'Persist crop for current session',
label: 'Current session',
cmd: [{
action: 'set-ar-persistence',
arg: CropModePersistence.CurrentSession,
}],
scopes: {
site: {
show: true,
},
global: {
show: true,
}
},
playerUi: {
show: true,
}
}, {
name: 'Persist until manually reset',
label: 'Always persist',
cmd: [{
action: 'set-ar-persistence',
arg: CropModePersistence.Forever,
}],
scopes: {
site: {
show: true,
},
global: {
show: true,
}
},
playerUi: {
show: true,
}
}, {
name: 'Default crop persistence',
label: 'Default',
cmd: [{
action: 'set-ar-persistence',
arg: CropModePersistence.Default,
}],
scopes: {
site: {
show: true,
},
},
playerUi: {
show: true,
}
}, {
name: 'Zoom in',
label: 'Zoom',
cmd: [{
action: 'change-zoom',
arg: 0.1
}],
scopes: {
page: {
show: false,
shortcut: [{
key: 'z',
code: 'KeyY',
ctrlKey: false,
metaKey: false,
altKey: false,
shiftKey: false,
onKeyUp: true,
onKeyDown: false,
}],
}
},
playerUi: {
show: false,
}
}, {
name: 'Zoom out',
label: 'Unzoom',
cmd: [{
action: 'change-zoom',
arg: -0.1
}],
scopes: {
page: {
show: false,
shortcut: [{
key: 'u',
code: 'KeyU',
ctrlKey: false,
metaKey: false,
altKey: false,
shiftKey: false,
onKeyUp: true,
onKeyDown: false,
}],
}
},
playerUi: {
show: false
}
}, {
name: 'Toggle panning mode',
label: 'Toggle pan',
cmd: [{
action: 'toggle-pan',
arg: 'toggle'
}],
playerUi: {
show: true,
path: 'zoom'
},
scopes: {
}
}, {
name: 'Hold to pan',
cmd: [{
action: 'pan',
arg: 'toggle',
}],
scopes: {
page: {
show: false,
shortcut: [{
ctrlKey: false,
metaKey: false,
altKey: false,
shiftKey: true,
onKeyDown: false,
onKeyUp: false,
onMouseMove: true,
}],
}
}
},
//
// S T R E T C H I N G
//
{
name: 'Set stretch to "none"',
label: 'Don\'t stretch',
cmd: [{
action: 'set-stretch',
arg: StretchType.NoStretch,
}],
scopes: {
global: {
show: true,
label: 'Normal'
},
site: {
show: true,
label: 'Normal'
},
page: {
show: true,
label: 'Normal'
}
},
playerUi: {
show: true,
path: 'stretch'
}
}, {
name: 'Set stretch to "basic"',
label: 'Basic stretch',
cmd: [{
action: 'set-stretch',
arg: StretchType.Basic,
}],
scopes: {
global: {
show: true,
label: 'Basic'
},
site: {
show: true,
label: 'Basic'
},
page: {
show: true,
label: 'Basic'
}
},
playerUi: {
show: true,
path: 'stretch'
}
}, {
name: 'Set stretch to "hybrid"',
label: 'Hybrid stretch',
cmd: [{
action: 'set-stretch',
arg: StretchType.Hybrid,
}],
scopes: {
global: {
show: true,
label: 'Hybrid'
},
site: {
show: true,
label: 'Hybrid'
},
page: {
show: true,
label: 'Hybrid'
}
},
playerUi: {
show: true,
path: 'stretch'
}
}, {
name: 'Stretch only to hide thin borders',
label: 'Thin borders only',
cmd: [{
action: 'set-stretch',
arg: StretchType.Conditional,
}],
scopes: {
global: {
show: true,
label: 'Thin borders'
},
site: {
show: true,
label: 'Thin borders'
},
page: {
show: true,
label: 'Thin borders'
}
},
playerUi: {
show: true,
path: 'stretch'
}
}, {
name: 'Set stretch to default value',
label: 'Default',
cmd: [{
action: 'set-stretch',
arg: StretchType.Default,
}],
scopes: {
site: {
show: true,
}
}
}, {
name: 'Stretch source to 4:3',
label: '4:3 stretch (src)',
cmd: [{
action: 'set-stretch',
arg: StretchType.FixedSource,
customArg: 1.33,
}],
scopes: {
page: {
show: true,
}
},
playerUi: {
show: true,
path: 'crop'
}
}, {
name: 'Stretch source to 16:9',
label: '16:9 stretch (src)',
cmd: [{
action: 'set-stretch',
arg: StretchType.FixedSource,
customArg: 1.77,
}],
scopes: {
page: {
show: true,
}
},
playerUi: {
show: true,
path: 'crop'
}
},
//
// A L I G N M E N T
//
{
name: 'Align video to the left',
label: 'Left',
cmd: [{
action: 'set-alignment',
arg: VideoAlignmentType.Left,
}],
scopes: {
global: {
show: true,
},
site: {
show: true,
},
page: {
show: true,
}
},
playerUi: {
show: true,
path: 'align'
}
}, {
name: 'Align video to center',
label: 'Center',
cmd: [{
action: 'set-alignment',
arg: VideoAlignmentType.Center,
}],
scopes: {
global: {
show: true,
},
site: {
show: true,
},
page: {
show: true,
}
},
playerUi: {
show: true,
path: 'align'
}
}, {
name: 'Align video to the right',
label: 'Right',
cmd: [{
action: 'set-alignment',
arg: VideoAlignmentType.Right
}],
scopes: {
global: {
show: true,
},
site: {
show: true,
},
page: {
show: true,
}
},
playerUi: {
show: true,
path: 'align'
}
}, {
name: 'Use default alignment',
label: 'Default',
cmd: [{
action: 'set-alignment',
arg: VideoAlignmentType.Default
}],
scopes: {
site: {
show: true,
}
}
},
//
// E N A B L E E X T E N S I O N / A U T O A R
// (for sites/extension tab in the popup)
//
{
name: 'Enable extension',
label: 'Enable',
cmd: [{
action: 'set-extension-mode',
arg: ExtensionMode.Enabled,
persistent: true,
}],
scopes: {
global: {
show: true,
},
site: {
show: true,
}
}
}, {
name: 'Enable extension on whitelisted sites only',
label: 'On whitelist only',
cmd: [{
action: 'set-extension-mode',
arg: ExtensionMode.Whitelist,
persistent: true,
}],
scopes: {
global: {
show: true
}
}
}, {
name: 'Extension mode: use default settings',
label: 'Default',
cmd: [{
action: 'set-extension-mode',
arg: ExtensionMode.Default,
persistent: true,
}],
scopes: {
site: {
show: true
}
}
}, {
name: 'Disable extension',
label: 'Disable',
cmd: [{
action: 'set-extension-mode',
arg: ExtensionMode.Disabled,
persistent: true,
}],
scopes: {
global: {
show: true,
},
site: {
show: true,
}
}
}, {
name: 'Enable automatic aspect ratio detection',
label: 'Enable',
cmd: [{
action: 'set-autoar-mode',
arg: ExtensionMode.Enabled,
persistent: true,
}],
scopes: {
global: {
show: true
},
site: {
show: true
}
}
}, {
name: 'Enable automatic aspect ratio detection on whitelisted sites only',
label: 'On whitelist only',
cmd: [{
action: 'set-autoar-mode',
arg: ExtensionMode.Whitelist,
persistent: true,
}],
scopes: {
global: {
show: true,
}
}
}, {
name: 'Use default settings for automatic aspect ratio detection',
label: 'Default',
cmd: [{
action: 'set-autoar-mode',
arg: ExtensionMode.Default,
persistent: true,
}],
scopes: {
site: {
show: true,
}
}
}, {
name: 'Disable automatic aspect ratio detection',
label: 'Disable',
cmd: [{
action: 'set-autoar-mode',
arg: ExtensionMode.Disabled,
persistent: true,
}],
scopes: {
global: {
show: true,
},
site: {
show: true,
}
}
},
//
//
// Enable/disable keyboard shortcuts
//
{
name: 'Enable keyboard shortcuts',
label: 'Enable',
cmd: [{
action: 'set-keyboard',
arg: ExtensionMode.Enabled,
}],
scopes: {
global: {
show: true,
},
site: {
show: true,
},
page: {
show: true,
}
}
}, {
name: 'Enable keyboard shortcuts on whitelisted sites only',
label: 'On whitelist only',
cmd: [{
action: 'set-keyboard',
arg: ExtensionMode.Whitelist,
}],
scopes: {
global: {
show: true
},
}
}, {
name: 'Keyboard shortcuts mode: use default settings',
label: 'Default',
cmd: [{
action: 'set-keyboard',
arg: ExtensionMode.Default,
}],
scopes: {
site: {
show: true
}
}
}, {
name: 'Disable keyboard shortcuts',
label: 'Disable',
cmd: [{
action: 'set-keyboard',
arg: ExtensionMode.Disabled,
}],
scopes: {
global: {
show: true,
},
site: {
show: true,
},
page: {
show: true,
}
}
},
],
mitigations: {
zoomLimit: {
enabled: true,
@ -1519,6 +820,46 @@ const ExtensionConf: SettingsInterface = {
}
}
},
"www.youtube-nocookie.com": {
enable: {
fullscreen: ExtensionMode.Enabled,
theater: ExtensionMode.Enabled,
normal: ExtensionMode.Enabled,
},
enableAard: {
fullscreen: ExtensionMode.Enabled,
theater: ExtensionMode.Enabled,
normal: ExtensionMode.Enabled,
},
enableKeyboard: {
fullscreen: ExtensionMode.Enabled,
theater: ExtensionMode.Enabled,
normal: ExtensionMode.Enabled
},
enableUI: {
fullscreen: ExtensionMode.Enabled,
theater: ExtensionMode.Enabled,
normal: ExtensionMode.Disabled
},
override: false, // ignore value localStorage in favour of this
type: 'official', // is officially supported? (Alternatives are 'community' and 'user-defined')
defaultType: 'official', // if user mucks around with settings, type changes to 'user-defined'.
// We still want to know what the original type was, hence defaultType
activeDOMConfig: 'official',
DOMConfig: {
'official': {
type: 'official',
elements: {
player: {
manual: true,
querySelectors: "#movie_player, #player, #c4-player",
}
}
}
}
},
"www.netflix.com" : {
enable: {
fullscreen: ExtensionMode.Enabled,

View File

@ -19,6 +19,10 @@ export interface EventBusContext {
frame?: any,
sourceFrame?: IframeData
forwardTo?: 'all' | 'active' | 'contentScript' | 'server' | 'sameOrigin' | 'popup' | 'all-frames',
};
borderCrossings?: {
commsServer?: boolean,
iframe?: boolean,
}
}
@ -29,6 +33,9 @@ export default class EventBus {
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}) {
@ -83,6 +90,18 @@ export default class EventBus {
}
}
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);
}
}
send(command: string, commandData: any, context?: EventBusContext) {
// execute commands we have subscriptions for
@ -94,8 +113,27 @@ export default class EventBus {
// preventing messages from flowing back to their original senders is
// CommsServer's job. EventBus does not have enough data for this decision.
if (this.comms) {
// We do, however, have enough data to prevent backflow of messages that
// crossed CommsServer once already.
if (this.comms && context?.origin !== CommsOrigin.Server && !context?.borderCrossings?.commsServer) {
this.comms.sendMessage({command, config: commandData, context}, context);
};
// call forwarding functions if they exist
if (!context?.borderCrossings?.iframe) {
for (const forwarding of this.iframeForwardingList) {
forwarding.fn(
command,
commandData,
{
...context,
borderCrossings: {
...context?.borderCrossings,
iframe: true
}
}
);
}
}
if (context?.stopPropagation) {

View File

@ -1,22 +1,33 @@
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 AspectRatioType from '../../common/enums/AspectRatioType.enum';
import { SiteSettings } from './settings/SiteSettings';
import { SettingsSnapshotManager } from './settings/SettingsSnapshotManager';
import { ComponentLogger } from './logging/ComponentLogger';
import { LogAggregator } from './logging/LogAggregator';
if(process.env.CHANNEL !== 'stable'){
console.info("Loading Settings");
}
interface SettingsOptions {
logAggregator: LogAggregator,
onSettingsChanged?: () => void,
afterSettingsSaved?: () => void,
activeSettings?: SettingsInterface,
}
interface SetSettingsOptions {
forcePreserveVersion?: boolean,
}
const SETTINGS_LOGGER_STYLES = {
log: 'color: #81d288',
}
class Settings {
//#region flags
@ -25,7 +36,9 @@ class Settings {
//#endregion
//#region helper classes
logger: Logger;
logAggregator: LogAggregator;
logger: ComponentLogger;
//#endregion
//#region data
@ -37,17 +50,23 @@ class Settings {
onSettingsChanged: any;
afterSettingsSaved: any;
onChangedCallbacks: any[] = [];
afterSettingsChangedCallbacks: any[] = [];
onChangedCallbacks: (() => void)[] = [];
afterSettingsChangedCallbacks: (() => void)[] = [];
public snapshotManager: SettingsSnapshotManager;
//#endregion
constructor(options) {
constructor(options: SettingsOptions) {
// Options: activeSettings, updateCallback, logger
this.logger = options?.logger;
this.onSettingsChanged = options?.onSettingsChanged;
this.afterSettingsSaved = options?.afterSettingsSaved;
this.active = options?.activeSettings ?? undefined;
this.logger = options.logAggregator && new ComponentLogger(options.logAggregator, 'Settings', {styles: SETTINGS_LOGGER_STYLES}) || undefined;;
this.onSettingsChanged = options.onSettingsChanged;
this.afterSettingsSaved = options.afterSettingsSaved;
this.active = options.activeSettings ?? undefined;
this.default = ExtensionConf;
this.snapshotManager = new SettingsSnapshotManager();
this.default['version'] = this.getExtensionVersion();
chrome.storage.onChanged.addListener((changes, area) => {this.storageChangeListener(changes, area)});
@ -57,14 +76,14 @@ class Settings {
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);
this.logger?.info('storageOnChange', "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);
this.logger?.info('storageOnChange', 'Does parsedSettings.preventReload exist?', parsedSettings.preventReload, "Does callback exist?", !!this.onSettingsChanged);
if (!parsedSettings.preventReload) {
try {
@ -72,23 +91,23 @@ class Settings {
try {
fn();
} catch (e) {
this.logger?.log('warn', 'settings', "[Settings] afterSettingsChanged fallback failed. It's possible that a vue component got destroyed, and this function is nothing more than vestigal remains. It would be nice if we implemented something that allows us to remove callback functions from array, and remove vue callbacks from the callback array when their respective UI component gets destroyed. Or this could be an error with the function itself. IDK, here's the error.", e)
this.logger?.warn('storageOnChange', "afterSettingsChanged fallback failed. It's possible that a vue component got destroyed, and this function is nothing more than vestigal remains. It would be nice if we implemented something that allows us to remove callback functions from array, and remove vue callbacks from the callback array when their respective UI component gets destroyed. Or this could be an error with the function itself. IDK, here's the error.", e)
}
}
if (this.onSettingsChanged) {
this.onSettingsChanged();
}
this.logger?.log('info', 'settings', '[Settings] Update callback finished.')
this.logger?.info('storageOnChange', 'Update callback finished.')
} catch (e) {
this.logger?.log('error', 'settings', "[Settings] CALLING UPDATE CALLBACK FAILED. Reason:", e)
this.logger?.error('storageOnChange', "CALLING UPDATE CALLBACK FAILED. Reason:", e)
}
}
for (const fn of this.afterSettingsChangedCallbacks) {
try {
fn();
} catch (e) {
this.logger?.log('warn', 'settings', "[Settings] afterSettingsChanged fallback failed. It's possible that a vue component got destroyed, and this function is nothing more than vestigal remains. It would be nice if we implemented something that allows us to remove callback functions from array, and remove vue callbacks from the callback array when their respective UI component gets destroyed. Or this could be an error with the function itself. IDK, here's the error.", e)
this.logger?.warn('storageOnChange', "afterSettingsChanged fallback failed. It's possible that a vue component got destroyed, and this function is nothing more than vestigal remains. It would be nice if we implemented something that allows us to remove callback functions from array, and remove vue callbacks from the callback array when their respective UI component gets destroyed. Or this could be an error with the function itself. IDK, here's the error.", e)
}
}
if (this.afterSettingsSaved) {
@ -171,34 +190,38 @@ class Settings {
return patchesIn.sort( (a, b) => this.compareExtensionVersions(a.forVersion, b.forVersion));
}
private findFirstNecessaryPatch(version, extconfPatches) {
const sorted = this.sortConfPatches(extconfPatches);
return sorted.findIndex(x => this.compareExtensionVersions(x.forVersion, version) > 0);
private findFirstNecessaryPatch(version) {
return ExtensionConfPatch.findIndex(x => this.compareExtensionVersions(x.forVersion, version) > 0);
}
private applySettingsPatches(oldVersion, patches) {
let index = this.findFirstNecessaryPatch(oldVersion, patches);
private applySettingsPatches(oldVersion) {
let index = this.findFirstNecessaryPatch(oldVersion);
if (index === -1) {
this.logger?.log('info','settings','[Settings::applySettingsPatches] There are no pending conf patches.');
this.logger?.info('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;
// save current settings object
const currentSettings = this.active;
if (Object.keys(patches[index]).length > 0) {
ObjectCopy.overwrite(this.active, patches[index]);
this.snapshotManager.createSnapshot(
JSON.parse(JSON.stringify(currentSettings)),
{
label: 'Pre-migration snapshot',
isAutomatic: true
}
if (updateFn) {
);
// apply all remaining patches
this.logger?.info('applySettingsPatches', `There are ${ExtensionConfPatch.length - index} settings patches to apply`);
while (index < ExtensionConfPatch.length) {
const updateFn = ExtensionConfPatch[index].updateFn;
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);
this.logger?.error('applySettingsPatches', 'Failed to execute update function. Keeping settings object as-is. Error:', e);
}
}
@ -207,36 +230,35 @@ class Settings {
}
async init() {
const settings = await this.get();
let settings = await this.get();
if (settings?.dev?.loadFromSnapshot) {
this.logger?.info('init', 'Dev mode is enabled, Loading settings from snapshot:', settings.dev.loadFromSnapshot);
const snapshot = await this.snapshotManager.getSnapshot();
if (snapshot) {
settings = snapshot.settings;
}
}
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;
const oldVersion = settings?.version ?? this.version;
if (settings) {
this.logger?.log('info', 'settings', "[Settings::init] Configuration fetched from storage:", settings,
this.logger?.info('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:',
this.logger?.info(
'init',
'settings don\'t exist. Using defaults.\n#keys:',
settings ? Object.keys(settings).length : 0,
'\nsettings:',
settings
@ -244,6 +266,7 @@ class Settings {
this.active = this.getDefaultSettings();
this.active.version = this.version;
await this.save();
return this.active;
}
@ -260,7 +283,7 @@ class Settings {
// 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.");
this.logger?.info('init', "extension was saved with current version of ultrawidify. Returning object as-is.");
return this.active;
}
@ -271,14 +294,14 @@ class Settings {
// 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);
this.logger?.info('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);
this.applySettingsPatches(oldVersion);
// set 'whatsNewChecked' flag to false when updating, always
this.active.whatsNewChecked = false;
@ -289,26 +312,25 @@ class Settings {
return this.active;
}
async get() {
async get(): Promise<SettingsInterface | undefined> {
let ret;
ret = await chrome.storage.local.get('uwSettings');
this.logger?.log('info', 'settings', 'Got settings:', ret && ret.uwSettings && JSON.parse(ret.uwSettings));
this.logger?.info('get', 'Got settings:', ret && ret.uwSettings && JSON.parse(ret.uwSettings));
try {
return JSON.parse(ret.uwSettings);
return JSON.parse(ret.uwSettings) as SettingsInterface;
} catch(e) {
return undefined;
}
}
async set(extensionConf, options?) {
async set(extensionConf, options?: SetSettingsOptions) {
if (!options || !options.forcePreserveVersion) {
extensionConf.version = this.version;
}
this.logger?.log('info', 'settings', "[Settings::set] setting new settings:", extensionConf)
this.logger?.info('set', "setting new settings:", extensionConf)
return chrome.storage.local.set( {'uwSettings': JSON.stringify(extensionConf)});
}
@ -351,18 +373,20 @@ class Settings {
}
}
async save(options?) {
async save(options?: SetSettingsOptions) {
if (Debug.debug && Debug.storage) {
console.log("[Settings::save] Saving active settings:", this.active);
console.log("[Settings::save] Saving active settings — save options", options, "; settings:", this.active);
}
this.active.preventReload = undefined;
this.active.lastModified = new Date();
await this.set(this.active, options);
}
async saveWithoutReload() {
async saveWithoutReload(options?: SetSettingsOptions) {
this.active.preventReload = true;
await this.set(this.active);
this.active.lastModified = new Date();
await this.set(this.active, options);
}
async rollback() {
@ -396,9 +420,15 @@ class Settings {
listenOnChange(fn: () => void): void {
this.onChangedCallbacks.push(fn);
}
removeOnChangeListener(fn: () => void): void {
this.onChangedCallbacks = this.afterSettingsChangedCallbacks.filter(x => x !== fn);
}
listenAfterChange(fn: () => void): void {
this.afterSettingsChangedCallbacks.push(fn);
}
removeAfterChangeListener(fn: () => void): void {
this.afterSettingsChangedCallbacks = this.afterSettingsChangedCallbacks.filter(x => x !== fn);
}
}
export default Settings;

View File

@ -1,20 +1,24 @@
import AspectRatioType from '../../../common/enums/AspectRatioType.enum';
import ExtensionMode from '../../../common/enums/ExtensionMode.enum';
import { ExtensionEnvironment } from '../../../common/interfaces/SettingsInterface';
import AspectRatioType from '@src/common/enums/AspectRatioType.enum';
import ExtensionMode from '@src/common/enums/ExtensionMode.enum';
import { ArVariant } from '@src/common/interfaces/ArInterface';
import { ExtensionEnvironment } from '@src/common/interfaces/SettingsInterface';
import EventBus from '../EventBus';
import Logger from '../Logger';
import Settings from '../Settings';
import { SiteSettings } from '../settings/SiteSettings';
import VideoData from '../video-data/VideoData';
import { AardDebugUi } from './AardDebugUi';
import { AardTimer } from './AardTimers';
import { Corner } from './enums/corner.enum';
import { VideoPlaybackState } from './enums/video-playback-state.enum';
import { FallbackCanvas } from './gl/FallbackCanvas';
import { GlCanvas } from './gl/GlCanvas';
import { GlDebugCanvas, GlDebugType } from './gl/GlDebugCanvas';
import { AardCanvasStore } from './interfaces/aard-canvas-store.interface';
import { AardDetectionSample, generateSampleArray, resetSamples } from './interfaces/aard-detection-sample.interface';
import { AardStatus, initAardStatus } from './interfaces/aard-status.interface';
import { AardTestResults, initAardTestResults, resetAardTestResults, resetGuardLine } from './interfaces/aard-test-results.interface';
import { AardTimers, initAardTimers } from './interfaces/aard-timers.interface';
import { ComponentLogger } from '../logging/ComponentLogger';
/**
@ -216,14 +220,14 @@ import { AardTimers, initAardTimers } from './interfaces/aard-timers.interface';
*
*/
export class Aard {
//#region configuration parameters
private logger: Logger;
private logger: ComponentLogger;
private videoData: VideoData;
private settings: Settings;
private siteSettings: SiteSettings;
private eventBus: EventBus;
private arid: string;
private arVariant: ArVariant;
private eventBusCommands = {
'uw-environment-change': {
@ -231,6 +235,15 @@ export class Aard {
console.log('received extension environment:', newEnvironment, 'player env:', this.videoData?.player?.environment);
this.startCheck();
}
},
'aard-enable-debug': {
function: (enabled: boolean) => {
if (enabled) {
this.showDebugCanvas();
} else {
this.hideDebugCanvas();
}
}
}
// 'get-aard-timing': {
// function: () => this.handlePerformanceDataRequest()
@ -249,9 +262,15 @@ export class Aard {
private fallbackReason: any;
private canvasStore: AardCanvasStore;
private testResults: AardTestResults;
private verticalTestResults: AardTestResults;
private canvasSamples: AardDetectionSample;
private forceFullRecheck: boolean = true;
private debugConfig: any = {};
private timer: AardTimer;
private lastAnimationFrameTime: number = Infinity;
//#endregion
//#region getters
@ -273,7 +292,7 @@ export class Aard {
//#region lifecycle
constructor(videoData: VideoData){
this.logger = videoData.logger;
this.logger = new ComponentLogger(videoData.logAggregator, 'Aard', {});
this.videoData = videoData;
this.video = videoData.video;
this.settings = videoData.settings;
@ -285,8 +304,9 @@ export class Aard {
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}`);
this.logger.log('ctor', `creating new ArDetector. arid: ${this.arid}`);
this.timer = new AardTimer();
this.init();
}
@ -311,16 +331,31 @@ export class Aard {
),
};
// try {
// this.showDebugCanvas();
// } catch (e) {
// console.error('FALIED TO CREATE DEBUGG CANVAS', e);
// }
try {
if (this.settings.active.ui.dev?.aardDebugOverlay?.showOnStartup) {
this.showDebugCanvas();
}
} catch (e) {
console.error(`[uw::aard] failed to create debug UI:`, e);
}
this.startCheck();
}
private createCanvas(canvasId: string, canvasType?: 'webgl' | 'fallback') {
private createCanvas(canvasId: string, canvasType?: 'webgl' | 'legacy') {
if (canvasType) {
if (canvasType === this.settings.active.arDetect.aardType || this.settings.active.arDetect.aardType === 'auto') {
if (canvasType === 'webgl') {
return new GlCanvas({...this.settings.active.arDetect.canvasDimensions.sampleCanvas, id: 'main-gl'});
} else if (canvasType === 'fallback') {
return new FallbackCanvas({...this.settings.active.arDetect.canvasDimensions.sampleCanvas, id: 'main-fallback'});
} else if (canvasType === 'legacy') {
return new FallbackCanvas({...this.settings.active.arDetect.canvasDimensions.sampleCanvas, id: 'main-legacy'});
} else {
// TODO: throw error
}
@ -335,28 +370,58 @@ export class Aard {
return new GlCanvas({...this.settings.active.arDetect.canvasDimensions.sampleCanvas, id: 'main-gl'});
} catch (e) {
if (this.settings.active.arDetect.aardType !== 'webgl') {
return new FallbackCanvas({...this.settings.active.arDetect.canvasDimensions.sampleCanvas, id: 'main-fallback'});
return new FallbackCanvas({...this.settings.active.arDetect.canvasDimensions.sampleCanvas, id: 'main-legacy'});
}
console.error('[ultrawidify|Aard::createCanvas] could not create webgl canvas:', e);
this.logger.error('createCanvas', 'could not create webgl canvas:', e);
this.eventBus.send('uw-config-broadcast', {type: 'aard-error', aardErrors: {webglError: true}});
throw e;
}
} else if (this.settings.active.arDetect.aardType === 'legacy') {
return new FallbackCanvas({...this.settings.active.arDetect.canvasDimensions.sampleCanvas, id: 'main-fallback'});
return new FallbackCanvas({...this.settings.active.arDetect.canvasDimensions.sampleCanvas, id: 'main-legacy'});
} else {
console.error('[ultrawidify|Aard::createCanvas] invalid value in settings.arDetect.aardType:', this.settings.active.arDetect.aardType);
this.logger.error('createCanvas', 'invalid value in settings.arDetect.aardType:', this.settings.active.arDetect.aardType);
this.eventBus.send('uw-config-broadcast', {type: 'aard-error', aardErrors: {invalidSettings: true}});
throw 'AARD_INVALID_SETTINGS';
}
}
/**
* Creates and shows debug canvas
* @param canvasId
*/
private showDebugCanvas() {
if (!this.canvasStore.debug) {
this.canvasStore.debug = new GlDebugCanvas({...this.settings.active.arDetect.canvasDimensions.sampleCanvas, id: 'uw-debug-gl'});
}
this.canvasStore.debug.enableFx();
if (!this.debugConfig.debugUi) {
this.debugConfig.debugUi = new AardDebugUi(this);
this.debugConfig.debugUi.initContainer();
this.debugConfig.debugUi.attachCanvases(this.canvasStore.main.canvas, this.canvasStore.debug.canvas);
// if we don't draw a dummy frame from _real_ sources, we can't update buffer later
this.canvasStore.debug.drawVideoFrame(this.canvasStore.main.canvas);
}
}
private hideDebugCanvas() {
if (this.debugConfig.debugUi) {
this.debugConfig?.debugUi.destroyContainer();
this.debugConfig.debugUi = undefined;
}
}
//#endregion
/**
* Checks whether autodetection can run
*/
startCheck() {
startCheck(arVariant?: ArVariant) {
this.arVariant = arVariant;
if (!this.videoData.player) {
console.warn('Player not detected!')
// console.warn('Player not detected!');
// console.log('--- video data: ---\n', this.videoData);
return;
}
if (this.siteSettings.data.enableAard[this.videoData.player.environment] === ExtensionMode.Enabled) {
this.start();
@ -377,6 +442,7 @@ export class Aard {
// do full reset of test samples
this.testResults = initAardTestResults(this.settings.active.arDetect);
this.verticalTestResults = initAardTestResults(this.settings.active.arDetect);
if (this.animationFrame) {
window.cancelAnimationFrame(this.animationFrame);
@ -390,8 +456,14 @@ export class Aard {
* Runs autodetection ONCE.
* If autodetection loop is running, this will also stop autodetection loop.
*/
step() {
step(options?: {noCache?: boolean}) {
this.stop();
if (options?.noCache) {
this.testResults = initAardTestResults(this.settings.active.arDetect);
this.verticalTestResults = initAardTestResults(this.settings.active.arDetect);
}
this.main();
}
@ -453,14 +525,20 @@ export class Aard {
*/
private async main() {
try {
this.timer.next();
let imageData: Uint8Array;
this.timer.current.start = performance.now();
// We abuse a do-while loop to eat our cake (get early returns)
// and have it, too (if we return early, we still execute code
// at the end of this function)
do {
const imageData = await new Promise<Uint8Array>(
imageData = await new Promise<Uint8Array>(
resolve => {
try {
this.canvasStore.main.drawVideoFrame(this.video);
this.timer.current.draw = performance.now() - this.timer.current.start;
resolve(this.canvasStore.main.getImageData());
} catch (e) {
if (e.name === 'SecurityError') {
@ -478,7 +556,7 @@ export class Aard {
} else {
if (this.settings.active.arDetect.aardType === 'auto') {
this.canvasStore.main.destroy();
this.canvasStore.main = this.createCanvas('main-gl', 'fallback');
this.canvasStore.main = this.createCanvas('main-gl', 'legacy');
}
this.inFallback = true;
this.fallbackReason = {cors: true};
@ -490,6 +568,7 @@ export class Aard {
}
}
);
this.timer.current.getImage = performance.now() - this.timer.current.start;
// STEP 1:
// Test if corners are black. If they're not, we can immediately quit the loop.
@ -498,6 +577,8 @@ export class Aard {
this.settings.active.arDetect.canvasDimensions.sampleCanvas.width,
this.settings.active.arDetect.canvasDimensions.sampleCanvas.height
);
this.timer.current.fastBlackLevel = performance.now() - this.timer.current.start;
if (this.testResults.notLetterbox) {
// TODO: reset aspect ratio to "AR not applied"
this.testResults.lastStage = 1;
@ -543,6 +624,7 @@ export class Aard {
);
}
}
this.timer.current.guardLine = performance.now() - this.timer.current.start; // guardLine is for both guardLine and imageLine checks
// Both need to be checked
if (! (this.testResults.imageLine.invalidated || this.testResults.guardLine.invalidated)) {
@ -569,9 +651,17 @@ export class Aard {
// If forceFullRecheck is set, then 'not letterbox' should always force-reset the aspect ratio
// (as aspect ratio may have been set manually while autodetection was off)
// If debugging is enable,
this.canvasStore.debug?.drawBuffer(imageData);
do {
if (this.testResults.notLetterbox) {
// console.log('————not letterbox')
// console.warn('DETECTED NOT LETTERBOX! (resetting)')
this.timer.arChanged();
this.updateAspectRatio(this.defaultAr);
break;
}
// if detection is uncertain, we don't do anything at all (unless if guardline was broken, in which case we reset)
@ -579,11 +669,11 @@ export class Aard {
// console.info('aspect ratio not certain:', this.testResults.aspectRatioUncertainReason);
// console.warn('check finished:', JSON.parse(JSON.stringify(this.testResults)), JSON.parse(JSON.stringify(this.canvasSamples)), '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n');
if (this.testResults.guardLine.invalidated) {
// console.warn('ASPECT RATIO UNCERTAIN, GUARD LINE INVALIDATED (resetting)')
this.timer.arChanged();
this.updateAspectRatio(this.defaultAr);
}
return;
break;
}
// TODO: emit debug values if debugging is enabled
@ -599,17 +689,30 @@ export class Aard {
// except aspectRatioUpdated doesn't get set reliably, so we just call update every time, and update
// if detected aspect ratio is different from the current aspect ratio
// if (this.testResults.aspectRatioUpdated) {
this.updateAspectRatio();
// this.timer.arChanged();
const finalAr = this.getAr();
if (finalAr > 0) {
this.updateAspectRatio(finalAr);
} else {
this.testResults.aspectRatioInvalid = true;
this.testResults.aspectRatioInvalidReason = finalAr.toFixed(3);
}
// }
// if we got "no letterbox" OR aspectRatioUpdated
} while (false)
if (this.canvasStore.debug) {
// this.canvasStore.debug.drawBuffer(imageData);
this.timer.getAverage();
this.debugConfig?.debugUi?.updateTestResults(this.testResults);
}
} catch (e) {
console.warn('[Ultrawidify] Aspect ratio autodetection crashed for some reason.\n\nsome reason:', e);
this.videoData.resizer.setAr({type: AspectRatioType.AutomaticUpdate, ratio: this.defaultAr});
this.videoData.resizer.setAr({type: AspectRatioType.AutomaticUpdate, ratio: this.defaultAr, variant: this.arVariant});
}
}
private getVideoPlaybackState(): VideoPlaybackState {
try {
if (this.video.ended) {
@ -622,7 +725,7 @@ export class Aard {
return VideoPlaybackState.Playing;
}
} catch (e) {
this.logger.log('warn', 'debug', `[ArDetect::getVideoPlaybackState] There was an error while determining video playback state.`, e);
this.logger.warn('getVideoPlaybackState]', `There was an error while determining video playback state.`, e);
return VideoPlaybackState.Error;
}
}
@ -682,11 +785,13 @@ export class Aard {
pixelValues[pvi++] = imageData[px_r];
pixelValues[pvi++] = imageData[px_r + 1];
pixelValues[pvi++] = imageData[px_r + 2];
imageData[px_r + 3] = GlDebugType.BlackLevelSample;
const endpx_r = px_r + (width * 4) - (i * 8) - 4; // -4 because 4 bytes per pixel, and - twice the offset to mirror the diagonal
pixelValues[pvi++] = imageData[endpx_r];
pixelValues[pvi++] = imageData[endpx_r + 1];
pixelValues[pvi++] = imageData[endpx_r + 2];
imageData[endpx_r + 3] = GlDebugType.BlackLevelSample;
}
// now let's populate the bottom two corners
@ -697,11 +802,13 @@ export class Aard {
pixelValues[pvi++] = imageData[px_r];
pixelValues[pvi++] = imageData[px_r + 1];
pixelValues[pvi++] = imageData[px_r + 2];
imageData[px_r + 3] = GlDebugType.BlackLevelSample;
const endpx_r = px_r + (width * 4) - (i * 8) - 4; // -4 because 4 bytes per pixel, and - twice the offset to mirror the diagonal
pixelValues[pvi++] = imageData[endpx_r];
pixelValues[pvi++] = imageData[endpx_r + 1];
pixelValues[pvi++] = imageData[endpx_r + 2];
imageData[endpx_r + 3] = GlDebugType.BlackLevelSample;
}
let min = 255;
@ -796,7 +903,10 @@ export class Aard {
|| imageData[i + 1] > this.testResults.blackThreshold
|| imageData[i + 2] > this.testResults.blackThreshold
) {
imageData[i + 3] = GlDebugType.GuardLineCornerViolation;
this.testResults.guardLine.cornerPixelsViolated[Corner.TopLeft]++;
} else {
imageData[i + 3] = GlDebugType.GuardLineCornerOk;
}
i += 4;
}
@ -806,12 +916,15 @@ export class Aard {
|| imageData[i + 1] > this.testResults.blackThreshold
|| imageData[i + 2] > this.testResults.blackThreshold
) {
imageData[i + 3] = GlDebugType.GuardLineViolation;
// DONT FORGET TO INVALIDATE GUARDL LINE
this.testResults.guardLine.top = -1;
this.testResults.guardLine.bottom = -1;
this.testResults.guardLine.invalidated = true;
return;
};
} else {
imageData[i + 3] = GlDebugType.GuardLineOk;
}
i += 4;
}
while (i < rowEnd) {
@ -820,7 +933,10 @@ export class Aard {
|| imageData[i + 1] > this.testResults.blackThreshold
|| imageData[i + 2] > this.testResults.blackThreshold
) {
imageData[i + 3] = GlDebugType.GuardLineCornerViolation;
this.testResults.guardLine.cornerPixelsViolated[Corner.TopRight]++;
} else {
imageData[i + 3] = GlDebugType.GuardLineCornerOk;
}
i += 4; // skip over alpha channel
}
@ -842,7 +958,10 @@ export class Aard {
|| imageData[i + 1] > this.testResults.blackThreshold
|| imageData[i + 2] > this.testResults.blackThreshold
) {
imageData[i + 3] = GlDebugType.GuardLineCornerViolation;
this.testResults.guardLine.cornerPixelsViolated[Corner.BottomLeft]++;
} else {
imageData[i + 3] = GlDebugType.GuardLineCornerOk;
}
i += 4; // skip over alpha channel
}
@ -855,12 +974,15 @@ export class Aard {
|| imageData[i + 1] > this.testResults.blackThreshold
|| imageData[i + 2] > this.testResults.blackThreshold
) {
imageData[i + 3] = GlDebugType.GuardLineViolation;
// DONT FORGET TO INVALIDATE GUARDL LINE
this.testResults.guardLine.top = -1;
this.testResults.guardLine.bottom = -1;
this.testResults.guardLine.invalidated = true;
return;
};
} else {
imageData[i + 3] = GlDebugType.GuardLineOk;
}
i += 4;
}
if (i % 4) {
@ -872,7 +994,10 @@ export class Aard {
|| imageData[i + 1] > this.testResults.blackThreshold
|| imageData[i + 2] > this.testResults.blackThreshold
) {
imageData[i + 3] = GlDebugType.GuardLineCornerViolation;
this.testResults.guardLine.cornerPixelsViolated[Corner.BottomRight]++;
} else {
imageData[i + 3] = GlDebugType.GuardLineCornerOk;
}
i += 4; // skip over alpha channel
}
@ -953,8 +1078,11 @@ export class Aard {
imagePixel ||= imageData[i++] > this.testResults.blackThreshold;
if (imagePixel && ++pixelCount > detectionThreshold) {
imageData[i] = GlDebugType.ImageLineThresholdReached;
return;
};
} else {
imageData[i] = imagePixel ? GlDebugType.ImageLineOk : GlDebugType.ImageLineFail;
}
i++; // skip over alpha channel
}
}
@ -965,8 +1093,11 @@ export class Aard {
imagePixel ||= imageData[i++] > this.testResults.blackThreshold;
if (imagePixel && ++pixelCount > detectionThreshold) {
imageData[i] = GlDebugType.ImageLineThresholdReached;
return;
};
} else {
imageData[i] = imagePixel ? GlDebugType.ImageLineOk : GlDebugType.ImageLineFail;
}
i++; // skip over alpha channel
}
if (! this.testResults.guardLine.cornerViolated[Corner.TopRight]) {
@ -977,8 +1108,11 @@ export class Aard {
imagePixel ||= imageData[i++] > this.testResults.blackThreshold;
if (imagePixel && ++pixelCount > detectionThreshold) {
imageData[i] = GlDebugType.ImageLineThresholdReached;
return;
};
} else {
imageData[i] = imagePixel ? GlDebugType.ImageLineOk : GlDebugType.ImageLineFail;
}
i++; // skip over alpha channel
}
}
@ -993,8 +1127,11 @@ export class Aard {
imagePixel ||= imageData[i++] > this.testResults.blackThreshold;
if (imagePixel && ++pixelCount > detectionThreshold) {
imageData[i] = GlDebugType.ImageLineThresholdReached;
return;
};
} else {
imageData[i] = imagePixel ? GlDebugType.ImageLineOk : GlDebugType.ImageLineFail;
}
i++; // skip over alpha channel
}
}
@ -1017,8 +1154,11 @@ export class Aard {
imagePixel ||= imageData[i++] > this.testResults.blackThreshold;
if (imagePixel && ++pixelCount > detectionThreshold) {
imageData[i] = GlDebugType.ImageLineThresholdReached;
return;
};
} else {
imageData[i] = imagePixel ? GlDebugType.ImageLineOk : GlDebugType.ImageLineFail;
}
i++; // skip over alpha channel
}
}
@ -1043,8 +1183,11 @@ export class Aard {
imagePixel ||= imageData[i++] > this.testResults.blackThreshold;
if (imagePixel && ++pixelCount > detectionThreshold) {
imageData[i] = GlDebugType.ImageLineThresholdReached;
return;
};
} else {
imageData[i] = imagePixel ? GlDebugType.ImageLineOk : GlDebugType.ImageLineFail;
}
i++; // skip over alpha channel
}
}
@ -1055,8 +1198,11 @@ export class Aard {
imagePixel ||= imageData[i++] > this.testResults.blackThreshold;
if (imagePixel && ++pixelCount > detectionThreshold) {
imageData[i] = GlDebugType.ImageLineThresholdReached;
return;
};
} else {
imageData[i] = imagePixel ? GlDebugType.ImageLineOk : GlDebugType.ImageLineFail;
}
i++; // skip over alpha channel
}
if (! this.testResults.guardLine.cornerViolated[Corner.TopRight]) {
@ -1067,8 +1213,11 @@ export class Aard {
imagePixel ||= imageData[i++] > this.testResults.blackThreshold;
if (imagePixel && ++pixelCount > detectionThreshold) {
imageData[i] = GlDebugType.ImageLineThresholdReached;
return;
};
} else {
imageData[i] = imagePixel ? GlDebugType.ImageLineOk : GlDebugType.ImageLineFail;
}
i++; // skip over alpha channel
}
}
@ -1083,8 +1232,11 @@ export class Aard {
imagePixel ||= imageData[i++] > this.testResults.blackThreshold;
if (imagePixel && ++pixelCount > detectionThreshold) {
imageData[i] = GlDebugType.ImageLineThresholdReached;
return;
};
} else {
imageData[i] = imagePixel ? GlDebugType.ImageLineOk : GlDebugType.ImageLineFail;
}
i++; // skip over alpha channel
}
}
@ -1095,8 +1247,11 @@ export class Aard {
imagePixel ||= imageData[i++] > this.testResults.blackThreshold;
if (imagePixel && ++pixelCount > detectionThreshold) {
imageData[i] = GlDebugType.ImageLineThresholdReached;
return;
};
} else {
imageData[i] = imagePixel ? GlDebugType.ImageLineOk : GlDebugType.ImageLineFail;
}
i++; // skip over alpha channel
}
if (! this.testResults.guardLine.cornerViolated[Corner.TopRight]) {
@ -1107,8 +1262,11 @@ export class Aard {
imagePixel ||= imageData[i++] > this.testResults.blackThreshold;
if (imagePixel && ++pixelCount > detectionThreshold) {
imageData[i] = GlDebugType.ImageLineThresholdReached;
return;
};
} else {
imageData[i] = imagePixel ? GlDebugType.ImageLineOk : GlDebugType.ImageLineFail;
}
i++; // skip over alpha channel
}
}
@ -1150,12 +1308,14 @@ export class Aard {
// bit more nicely visible (instead of hidden among spagheti)
this.edgeScan(imageData, width, height);
this.validateEdgeScan(imageData, width, height);
this.timer.current.edgeScan = performance.now() - this.timer.current.start;
// TODO: _if gradient detection is enabled, then:
this.sampleForGradient(imageData, width, height);
this.timer.current.gradient = performance.now() - this.timer.current.start;
this.processScanResults(imageData, width, height);
this.timer.current.scanResults = performance.now() - this.timer.current.start;
}
/**
@ -1222,11 +1382,13 @@ export class Aard {
|| imageData[rowOffset + x + 2] > this.testResults.blackLevel;
if (!isImage) {
imageData[rowOffset + x + 3] = GlDebugType.EdgeScanProbe;
// TODO: maybe some day mark this pixel as checked by writing to alpha channel
i++;
continue;
}
if (this.canvasSamples.top[i] === -1) {
imageData[rowOffset + x + 3] = GlDebugType.EdgeScanHit;
this.canvasSamples.top[i] = row;
finishedRows++;
}
@ -1270,12 +1432,14 @@ export class Aard {
|| imageData[rowOffset + x + 2] > this.testResults.blackLevel;
if (!isImage) {
imageData[rowOffset + x + 3] = GlDebugType.EdgeScanProbe;
// console.log('(row:', row, ')', 'val:', imageData[rowOffset + x], 'col', x >> 2, x, 'pxoffset:', rowOffset + x, 'len:', imageData.length)
// TODO: maybe some day mark this pixel as checked by writing to alpha channel
i++;
continue;
}
if (this.canvasSamples.bottom[i] === -1) {
imageData[rowOffset + x + 3] = GlDebugType.EdgeScanHit;
this.canvasSamples.bottom[i] = row;
finishedRows++;
}
@ -1315,6 +1479,9 @@ export class Aard {
const slopeTestSample = this.settings.active.arDetect.edgeDetection.slopeTestWidth * 4;
while (i < this.canvasSamples.top.length) {
// if (this.canvasSamples.top[i] < 0) {
// continue;
// }
// calculate row offset:
row = (this.canvasSamples.top[i + 1] - 1) * width * 4;
xs = row + this.canvasSamples.top[i] - slopeTestSample;
@ -1326,8 +1493,11 @@ export class Aard {
|| imageData[xs + 1] > this.testResults.blackThreshold
|| imageData[xs + 2] > this.testResults.blackThreshold
) {
imageData[xs + 3] = GlDebugType.SlopeTestDarkViolation;
this.canvasSamples.top[i + 1] = -1;
break;
} else {
imageData[xs + 3] = GlDebugType.SlopeTestDarkOk;
}
xs += 4;
}
@ -1337,9 +1507,13 @@ export class Aard {
i = 0;
let i1 = 0;
while (i < this.canvasSamples.bottom.length) {
// if (this.canvasSamples.bottom[i] < 0) {
// continue;
// }
// calculate row offset:
i1 = i + 1;
row = (this.canvasSamples.bottom[i1] - 1) * width * 4;
row = (this.canvasSamples.bottom[i1] + 1) * width * 4;
xs = row + this.canvasSamples.bottom[i] - slopeTestSample;
xe = row + this.canvasSamples.bottom[i] + slopeTestSample;
@ -1349,15 +1523,17 @@ export class Aard {
|| imageData[xs + 1] > this.testResults.blackThreshold
|| imageData[xs + 2] > this.testResults.blackThreshold
) {
imageData[xs + 3] = GlDebugType.SlopeTestDarkViolation;
this.canvasSamples.bottom[i1] = -1;
i += 2;
break;
}
imageData[xs + 3] = GlDebugType.SlopeTestDarkOk;
xs += 4;
}
if (this.canvasSamples.bottom[i1]) {
this.canvasSamples.bottom[i1] = height - this.canvasSamples.bottom[i1];
this.canvasSamples.bottom[i1] = this.canvasSamples.bottom[i1];
}
i += 2;
@ -1381,6 +1557,10 @@ export class Aard {
upperEdgeCheck:
for (let i = 1; i < this.canvasSamples.top.length; i += 2) {
if (this.canvasSamples.top[i] < 0) {
continue;
}
pixelOffset = this.canvasSamples.top[i] * realWidth + this.canvasSamples.top[i - 1] * 4;
lastSubpixel = imageData[pixelOffset] > imageData[pixelOffset + 1] ? imageData[pixelOffset] : imageData[pixelOffset + 1];
@ -1423,6 +1603,9 @@ export class Aard {
lowerEdgeCheck:
for (let i = 1; i < this.canvasSamples.bottom.length; i += 2) {
if (this.canvasSamples.bottom[i] < 0) {
continue;
}
pixelOffset = (height - this.canvasSamples.bottom[i]) * realWidth + this.canvasSamples.bottom[i - 1] * 4;
lastSubpixel = imageData[pixelOffset] > imageData[pixelOffset + 1] ? imageData[pixelOffset] : imageData[pixelOffset + 1];
@ -1461,7 +1644,6 @@ export class Aard {
if (lastSubpixel - firstSubpixel > this.settings.active.arDetect.edgeDetection.gradientTestMinDelta) {
this.canvasSamples.bottom[i] = -1;
}
}
}
@ -1818,11 +2000,22 @@ export class Aard {
return;
}
if (maxOffset > 2) {
this.testResults.imageLine.top = this.testResults.aspectRatioCheck.topCandidate === Infinity ? -1 : this.testResults.aspectRatioCheck.topCandidate;
this.testResults.imageLine.bottom = this.testResults.aspectRatioCheck.bottomCandidate === Infinity ? -1 : this.testResults.aspectRatioCheck.bottomCandidate;
if (this.testResults.aspectRatioCheck.topCandidate === Infinity) {
this.testResults.imageLine.top = -1;
this.testResults.guardLine.top = -1;
} else {
this.testResults.imageLine.top = this.testResults.aspectRatioCheck.topCandidate = this.testResults.aspectRatioCheck.topCandidate;
this.testResults.guardLine.top = Math.max(this.testResults.imageLine.top - 2, 0);
}
if (this.testResults.aspectRatioCheck.bottomCandidate === Infinity) {
this.testResults.imageLine.bottom = -1;
this.testResults.guardLine.bottom = -1;
} else {
this.testResults.imageLine.bottom = this.testResults.aspectRatioCheck.bottomCandidate;
this.testResults.guardLine.bottom = Math.min(this.testResults.imageLine.bottom + 2, this.canvasStore.main.height - 1);
}
}
this.testResults.aspectRatioUncertain = false;
@ -1845,7 +2038,8 @@ export class Aard {
this.videoData.resizer.updateAr({
type: AspectRatioType.AutomaticUpdate,
ratio: this.getAr(),
offset: this.testResults.letterboxOffset
offset: this.testResults.letterboxOffset,
variant: this.arVariant
});
this.testResults.activeAspectRatio = ar;
}
@ -1859,7 +2053,7 @@ export class Aard {
const fileAr = this.video.videoWidth / this.video.videoHeight;
const canvasAr = this.canvasStore.main.width / this.canvasStore.main.height;
const compensatedWidth = fileAr === canvasAr ? this.canvasStore.main.width : this.canvasStore.main.width * fileAr;
const compensatedWidth = fileAr === canvasAr ? this.canvasStore.main.width : this.video.videoWidth * this.canvasStore.main.height / (this.video.videoHeight);
// console.log(`
// ———— ASPECT RATIO CALCULATION: —————

View File

@ -0,0 +1,418 @@
import { AardPerformanceData } from './AardTimers';
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%">
<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="background: black; color: #ccc; padding: 1rem">
<div>
<span style="color: rgb(0.1, 0.1, 0.35)"></span>
Black level sample
</div>
<div>
<span style="color: rgb(0.3, 1.0, 0.6)"></span>
<span style="color: rgb(0.1, 0.5, 0.3)"></span>
Guard line (middle/corner) OK
</div>
<div>
<span style="color: rgb(1.0, 0.1, 0.1)"></span>
<span style="color: rgb(0.5, 0.0, 0.0)"></span>
Guard line (middle/corner) violation
</div>
<div>
Image line <span style="color: rgb(0.7, 0.7, 0.7)"></span> image, <span style="color: rgb(0.2, 0.2, 0.6)"></span> no image
</div>
<div>
Edge scan <span style="color: rgb(0.1, 0.1, 0.4)"></span> probe, <span style="color: rgb(0.4, 0.4, 1.0)"></span> hit
</div>
<div>
Slope test <span style="color: rgb(0.4, 0.4, 1.0)"></span> ok, <span style="color: rgb(1.0, 0.0, 0.0)"></span> fail
</div>
</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">
<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 style="width: 100%; display: flex; flex-direction: column">
<div style="margin-bottom: 1rem;">
<span style="color: #fff">Raw times (cumulative; -1 = test was skipped):</span><br/>
draw <span style="color: #fa6; margin-right: 0.5rem;">${this.aard.timer.current.draw.toFixed(2)}ms</span>
get data <span style="color: #fa6; margin-right: 0.5rem;">${this.aard.timer.current.getImage.toFixed(2)}ms</span>
black lv. <span style="color: #fa6; margin-right: 0.5rem;">${this.aard.timer.current.fastBlackLevel.toFixed(2)}ms</span>
guard/image <span style="color: #fa6; margin-right: 0.5rem;">${this.aard.timer.current.guardLine.toFixed(3)}ms</span>
edge <span style="color: #fa6; margin-right: 0.5rem;">${this.aard.timer.current.edgeScan.toFixed(2)}ms</span>
gradient <span style="color: #fa6; margin-right: 0.5rem;">${this.aard.timer.current.gradient.toFixed(3)}ms</span>
post <span style="color: #fa6; margin-right: 0.5rem;">${this.aard.timer.current.scanResults.toFixed(2)}ms</span>
</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;">
<span style="color: #fff">Raw times (cumulative; -1 = test was skipped):</span><br/>
draw <span style="color: #fa6; margin-right: 0.5rem;">${this.aard.timer.average.draw.toFixed(2)}ms</span>
get data <span style="color: #fa6; margin-right: 0.5rem;">${this.aard.timer.average.getImage.toFixed(2)}ms</span>
black lv. <span style="color: #fa6; margin-right: 0.5rem;">${this.aard.timer.average.fastBlackLevel.toFixed(2)}ms</span>
guard/image <span style="color: #fa6; margin-right: 0.5rem;">${this.aard.timer.average.guardLine.toFixed(3)}ms</span>
edge <span style="color: #fa6; margin-right: 0.5rem;">${this.aard.timer.average.edgeScan.toFixed(2)}ms</span>
gradient <span style="color: #fa6; margin-right: 0.5rem;">${this.aard.timer.average.gradient.toFixed(3)}ms</span>
post <span style="color: #fa6; margin-right: 0.5rem;">${this.aard.timer.average.scanResults.toFixed(2)}ms</span>
</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="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>
</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 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 = Math.max(perf.guardLine - total, 0);
total += guardLine;
const edgeScanStart = guardLineStart + guardLine;
const edgeScan = Math.max(perf.edgeScan - total, 0);
total += edgeScan;
const gradientStart = edgeScanStart + edgeScan;
const gradient = Math.max(perf.gradient - total, 0);
total += gradient;
const scanResultsStart = gradientStart + gradient;
const scanResults = Math.max(perf.scanResults - total, 0);
total += scanResults;
return `
<div style="width: 100%; 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)}
${this.getBarLabel(0, scanResults + scanResultsStart, 88, `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>
`;
}
updateTestResults(testResults) {
this.updatePerformanceResults();
if (testResults.aspectRatioUpdated && this.pauseOnArCheck) {
(this.aard as any).video.pause();
this.aard.stop();
}
const resultsDiv = document.getElementById('uw-aard-results');
let out = `
LAST STAGE: ${testResults.lastStage} | black level: ${testResults.blackLevel}, threshold: ${testResults.blackThreshold}
-- ASPECT RATIO
Active: ${testResults.activeAspectRatio.toFixed(3)}, changed since last check? ${testResults.aspectRatioUpdated} letterbox width: ${testResults.letterboxWidth} offset ${testResults.letterboxOffset}
image in black level probe (aka "not letterbox"): ${testResults.notLetterbox}
`;
if (testResults.notLetterbox) {
resultsDiv.textContent = out;
return;
}
out = `${out}
-- UNCERTAIN FLAGS
AR: ${testResults.aspectRatioUncertain} (reason: ${testResults.aspectRatioUncertainReason ?? 'n/a'}); top row: ${testResults.topRowUncertain}; bottom row: ${testResults.bottomRowUncertain}${
testResults.aspectRatioInvalid ? `\nINVALID_AR (reason: ${testResults.aspectRatioInvalidReason ?? 'n/a'})` : ''}
-- GUARD & IMAGE LINE
bottom guard: ${testResults.guardLine.bottom} image: ${testResults.guardLine.invalidated ? 'n/a' : testResults.imageLine.bottom}
top guard: ${testResults.guardLine.top} image: ${testResults.guardLine.invalidated ? 'n/a' : testResults.imageLine.top}
guard line ${testResults.guardLine.invalidated ? 'INVALIDATED' : 'valid'} image line ${testResults.guardLine.invalidated ? '<skipped test>' : testResults.imageLine.invalidated ? 'INVALIDATED' : 'valid'}
corner invalidations (invalid pixels -> verdict)
LEFT CENTER RIGHT
bottom: ${testResults.guardLine.cornerPixelsViolated[0]} ${testResults.guardLine.cornerViolated[0] ? '❌' : '◽'} ${testResults.guardLine.cornerPixelsViolated[1]} ${testResults.guardLine.cornerViolated[1] ? '❌' : '◽'}
top: ${testResults.guardLine.cornerPixelsViolated[2]} ${testResults.guardLine.cornerViolated[2] ? '❌' : '◽'} ${testResults.guardLine.cornerPixelsViolated[3]} ${testResults.guardLine.cornerViolated[3] ? '❌' : '◽'}
-- AR SCAN ${testResults.lastStage < 1 ? `
DID NOT RUN THIS FRAME` : `
LEFT CENTER RIGHT CANDIDATE
BOTTOM
distance: ${testResults.aspectRatioCheck.bottomRows[0]} ${testResults.aspectRatioCheck.bottomRows[1]} ${testResults.aspectRatioCheck.bottomRows[2]} ${testResults.aspectRatioCheck.bottomCandidate}
quality: ${testResults.aspectRatioCheck.bottomQuality[0]} ${testResults.aspectRatioCheck.bottomQuality[1]} ${testResults.aspectRatioCheck.bottomQuality[2]} ${testResults.aspectRatioCheck.bottomCandidateQuality}
TOP
distance: ${testResults.aspectRatioCheck.topRows[0]} ${testResults.aspectRatioCheck.topRows[1]} ${testResults.aspectRatioCheck.topRows[2]} ${testResults.aspectRatioCheck.topCandidate}
quality: ${testResults.aspectRatioCheck.topQuality[0]} ${testResults.aspectRatioCheck.topQuality[1]} ${testResults.aspectRatioCheck.topQuality[2]} ${testResults.aspectRatioCheck.topCandidateQuality}
Diff matrix:
R-L C-R C-L
bottom: ${testResults.aspectRatioCheck.bottomRowsDifferenceMatrix[0]} ${testResults.aspectRatioCheck.bottomRowsDifferenceMatrix[1]} ${testResults.aspectRatioCheck.bottomRowsDifferenceMatrix[2]}
top: ${testResults.aspectRatioCheck.topRowsDifferenceMatrix[0]} ${testResults.aspectRatioCheck.topRowsDifferenceMatrix[1]} ${testResults.aspectRatioCheck.topRowsDifferenceMatrix[2]}
`}
`;
resultsDiv.textContent = 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' : '';
}
}

View File

@ -0,0 +1,112 @@
export interface AardPerformanceData {
start: number;
draw: number;
getImage: number;
fastBlackLevel: number;
guardLine: number; // actually times both guard line and image line checks
edgeScan: number; // includes validation step
gradient: number;
scanResults: 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
}
};
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;
}
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;
}
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 += sample.fastBlackLevel;
}
if (sample.guardLine !== -1) {
this.average.guardLine += sample.guardLine;
}
if (sample.edgeScan !== -1) {
this.average.edgeScan += sample.edgeScan;
}
if (sample.gradient !== -1) {
this.average.gradient += sample.gradient;
}
if (sample.scanResults !== -1) {
this.average.scanResults += sample.scanResults;
}
}
this.average.draw /= this.aardPerformanceDataBuffer.length;
this.average.getImage /= this.aardPerformanceDataBuffer.length;
this.average.fastBlackLevel /= this.aardPerformanceDataBuffer.length;
this.average.guardLine /= this.aardPerformanceDataBuffer.length;
this.average.edgeScan /= this.aardPerformanceDataBuffer.length;
this.average.gradient /= this.aardPerformanceDataBuffer.length;
this.average.scanResults /= this.aardPerformanceDataBuffer.length;
}
}

View File

@ -2,7 +2,9 @@ import { GlCanvas, GlCanvasOptions } from './GlCanvas';
export class FallbackCanvas extends GlCanvas {
get type() {
return 'legacy';
}
context: CanvasRenderingContext2D;
constructor(options: GlCanvasOptions) {
@ -24,7 +26,7 @@ export class FallbackCanvas extends GlCanvas {
protected initWebgl() { }
drawVideoFrame(video: HTMLVideoElement) {
this.context.drawImage(video, this.context.canvas.width, this.context.canvas.height);
this.context.drawImage(video, 0, 0, this.context.canvas.width, this.context.canvas.height);
}
getImageData() {

View File

@ -52,6 +52,9 @@ interface GlCanvasProgramInfo {
}
export class GlCanvas {
get type() {
return 'webgl';
}
private _canvas: HTMLCanvasElement;
private set canvas(x: HTMLCanvasElement) {
@ -65,7 +68,7 @@ export class GlCanvas {
private set gl(x: WebGLRenderingContext) {
this._context = x;
};
private get gl(): WebGLRenderingContext {
protected get gl(): WebGLRenderingContext {
return this._context;
}
@ -80,7 +83,7 @@ export class GlCanvas {
private buffers: GlCanvasBuffers;
private texture: WebGLTexture;
private programInfo: GlCanvasProgramInfo;
protected programInfo: GlCanvasProgramInfo;
private projectionMatrix: mat4;
get width() {
@ -103,7 +106,7 @@ export class GlCanvas {
* Draws video frame to the GL canvas
* @param video video to extract a frame from
*/
drawVideoFrame(video: HTMLVideoElement): void {
drawVideoFrame(video: HTMLVideoElement | HTMLCanvasElement): void {
this.updateTexture(video);
this.drawScene();
}
@ -156,8 +159,17 @@ export class GlCanvas {
);
if (!this.gl) {
try {
this.gl = this.canvas.getContext(
"webgl",
{
preserveDrawingBuffer: true
}
);
} catch (e) {
throw new Error('WebGL not supported');
}
}
if(options.id) {
this.canvas.setAttribute('id', options.id);
}
@ -212,24 +224,30 @@ export class GlCanvas {
this.frameBuffer = new Uint8Array(this.frameBufferSize);
}
private loadShader(type, source) {
protected loadShader(type, source) {
const shader = this.gl.createShader(type);
this.gl.shaderSource(shader, source);
this.gl.compileShader(shader);
// TODO: warn if shader failed to compile
if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) {
this.gl.deleteShader(shader);
console.warn('DEBUG: Shader Compilation Error: ', type, this.gl.getShaderInfoLog(shader), '(cheat sheet: vertex shaders:', this.gl.VERTEX_SHADER, ')');
return null;
}
return shader;
}
private initShaderProgram() {
protected loadShaders() {
const vertexShader = this.loadShader(this.gl.VERTEX_SHADER, vsSource);
const fragmentShader = this.loadShader(this.gl.FRAGMENT_SHADER, fsSource);
return {vertexShader, fragmentShader};
}
private initShaderProgram() {
const {vertexShader, fragmentShader} = this.loadShaders();
// Create the shader program
const shaderProgram = this.gl.createProgram();
this.gl.attachShader(shaderProgram, vertexShader);
@ -238,6 +256,7 @@ export class GlCanvas {
// TODO: maybe give a warning if program failed to initialize
if (!this.gl.getProgramParameter(shaderProgram, this.gl.LINK_STATUS)) {
console.warn('DEBUG — FAILED TO LINK SHADER PROGRAM', this.gl.getProgramInfoLog(shaderProgram))
return null;
}
@ -279,7 +298,7 @@ export class GlCanvas {
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.LINEAR);
}
private updateTexture(video: HTMLVideoElement) {
protected updateTexture(video: HTMLVideoElement | HTMLCanvasElement | null) {
const level = 0;
const internalFormat = this.gl.RGBA;
const srcFormat = this.gl.RGBA;
@ -333,7 +352,7 @@ export class GlCanvas {
this.gl.enableVertexAttribArray(this.programInfo.attribLocations.vertexPosition);
}
private drawScene(): void {
protected drawScene(): void {
/**
* Since we are drawing our frames in a way such that the entire canvas is
* always covered by rendered video, and given our video is the only object

View File

@ -0,0 +1,162 @@
import { GlCanvas, GlCanvasOptions } from './GlCanvas';
const vsSource = `
attribute vec4 aVertexPosition;
attribute vec3 aVertexNormal;
attribute vec2 aTextureCoord;
uniform mat4 uNormalMatrix;
uniform mat4 uModelViewMatrix;
uniform mat4 uProjectionMatrix;
varying highp vec2 vTextureCoord;
void main(void) {
gl_Position = uProjectionMatrix * aVertexPosition;
vTextureCoord = vec2(aTextureCoord.x, 1.0 - aTextureCoord.y);
// vTextureCoord = aTextureCoord;
}
`;
const fSource = `
precision mediump float;
uniform sampler2D u_texture;
// uniform sampler1D u_colorTexture; // Array of replacement colors
uniform vec3 u_colors[16];
varying vec2 vTextureCoord;
void main() {
vec4 color = texture2D(u_texture, vTextureCoord);
int alphaIndex = int(color.a * 255.0);
if (alphaIndex == 255) { // convert to grayscale on normal alpha
float gray = dot(color.rgb, vec3(0.299, 0.587, 0.114));
gl_FragColor = vec4(vec3(gray), 1.0);
} else if (alphaIndex < 16) { // use custom color where possible
// no 1D textures in webgl, only webgl2 maybe
// vec3 selectedColor = texture1D(u_colorTexture, float(alphaIndex) / 15.0).rgb;
// gl_FragColor = vec4(selectedColor, 1.0);
vec3 selectedColor;
if (alphaIndex == 0) selectedColor = u_colors[0];
else if (alphaIndex == 1) selectedColor = u_colors[1];
else if (alphaIndex == 2) selectedColor = u_colors[2];
else if (alphaIndex == 3) selectedColor = u_colors[3];
else if (alphaIndex == 4) selectedColor = u_colors[4];
else if (alphaIndex == 5) selectedColor = u_colors[5];
else if (alphaIndex == 6) selectedColor = u_colors[6];
else if (alphaIndex == 7) selectedColor = u_colors[7];
else if (alphaIndex == 8) selectedColor = u_colors[8];
else if (alphaIndex == 9) selectedColor = u_colors[9];
else if (alphaIndex == 10) selectedColor = u_colors[10];
else if (alphaIndex == 11) selectedColor = u_colors[11];
else if (alphaIndex == 12) selectedColor = u_colors[12];
else if (alphaIndex == 13) selectedColor = u_colors[13];
else if (alphaIndex == 14) selectedColor = u_colors[14];
else selectedColor = u_colors[15];
gl_FragColor = vec4(selectedColor, 1.0);
} else { // red channel only as fallback
gl_FragColor = vec4(color.r, 0.0, 0.0, 1.0);
}
}
`;
export enum GlDebugType {
BlackLevelSample = 0,
GuardLineOk = 1,
GuardLineViolation = 2,
GuardLineCornerOk = 3,
GuardLineCornerViolation = 4,
ImageLineThresholdReached = 5,
ImageLineOk = 6,
ImageLineFail = 7,
EdgeScanProbe = 8,
EdgeScanHit = 9,
SlopeTestDarkOk = 10,
SlopeTestDarkViolation = 11,
}
export class GlDebugCanvas extends GlCanvas {
private debugColors = [
0.1, 0.1, 0.35, // 0 - black level sample
0.3, 1.0, 0.6, // 1 - guard line ok
1.0, 0.1, 0.1, // 2 - guard line violation
0.1, 0.5, 0.3, // 3 - guard line corner ok
0.5, 0.0, 0.0, // 4 - guard line corner violation
1.0, 1.0, 1.0, // 5 - image line threshold reached (stop checking)
0.7, 0.7, 0.7, // 6 - image line ok
0.2, 0.2, 0.6, // 7 - image line fail
0.1, 0.1, 0.4, // 8 - edge scan probe
0.4, 0.4, 1.0, // 9 - edge scan hit
0.2, 0.4, 0.6, // 10 - slope test ok
1.0, 0.0, 0.0, // 11 - slope test fail
0.0, 0.0, 0.0, // 12
0.0, 0.0, 0.0,
0.0, 0.0, 0.0,
0.0, 0.0, 0.0,
];
constructor (options: GlCanvasOptions) {
super(options);
this.canvas.id = options.id;
}
protected loadShaders() {
const vertexShader = this.loadShader(this.gl.VERTEX_SHADER, vsSource);
const fragmentShader = this.loadShader(this.gl.FRAGMENT_SHADER, fSource);
return {vertexShader, fragmentShader};
}
show() {
this.enableFx();
// this.canvas.style.position = 'fixed';
// this.canvas.style.top = '0';
// this.canvas.style.right = '0';
// this.canvas.style.zIndex = '99999999';
// this.canvas.style.transform = 'scale(3)';
// this.canvas.style.transformOrigin = 'top right';
// this.canvas.style.imageRendering = 'pixelated';
// document.body.appendChild(
// this.canvas
// );
}
enableFx() {
this.gl.useProgram(this.programInfo.program)
this.gl.uniform3fv((this.programInfo.uniformLocations as any).debugColors, this.debugColors);
}
drawBuffer(buffer: Uint8Array) {
this.updateTextureBuffer(buffer);
}
protected initWebgl() {
super.initWebgl();
(this.programInfo.uniformLocations as any).debugColors = this.gl.getUniformLocation(this.programInfo.program, 'u_colors');
}
protected updateTextureBuffer(buffer: Uint8Array) {
// this.updateTexture(null);
this.gl.texSubImage2D(
this.gl.TEXTURE_2D,
0,
0,
0,
this.width,
this.height,
this.gl.RGBA,
this.gl.UNSIGNED_BYTE,
buffer
);
this.drawScene();
};
}

View File

@ -0,0 +1,83 @@
export interface GlCanvasBuffers {
position: WebGLBuffer,
normal: WebGLBuffer,
textureCoord: WebGLBuffer,
indices: WebGLBuffer,
};
export function initBuffers(gl: WebGLRenderingContext): GlCanvasBuffers {
const positionBuffer = initPositionBuffer(gl);
const textureCoordBuffer = initTextureBuffer(gl);
const indexBuffer = initIndexBuffer(gl);
const normalBuffer = initNormalBuffer(gl);
return {
position: positionBuffer,
normal: normalBuffer,
textureCoord: textureCoordBuffer,
indices: indexBuffer,
};
}
function initPositionBuffer(gl: WebGLRenderingContext) {
const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
const positions = [
-1, -1, 1, -1, -1, 1,
-1, 1, 1, -1, 1, 1
];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
return positionBuffer;
}
function initIndexBuffer(gl: WebGLRenderingContext) {
const indexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
const indices = [
0, 1, 2,
0, 2, 3,
];
gl.bufferData(
gl.ELEMENT_ARRAY_BUFFER,
new Uint16Array(indices),
gl.STATIC_DRAW
);
return indexBuffer;
}
function initTextureBuffer(gl: WebGLRenderingContext) {
const textureCoordBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, textureCoordBuffer);
const texCoordBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
const texCoords = [
0, 1, 1, 1, 0, 0,
0, 0, 1, 1, 1, 0
];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(texCoords), gl.STATIC_DRAW);
return textureCoordBuffer;
}
function initNormalBuffer(gl: WebGLRenderingContext) {
const normalBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer);
const vertexNormals = [
0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0,
];
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array(vertexNormals),
gl.STATIC_DRAW
);
return normalBuffer;
}

View File

@ -1,5 +1,7 @@
import { GlCanvas } from '../gl/GlCanvas';
import { GlDebugCanvas } from '../gl/GlDebugCanvas';
export interface AardCanvasStore {
main: GlCanvas;
debug?: GlDebugCanvas;
}

View File

@ -39,7 +39,9 @@ export interface AardTestResults {
letterboxWidth: number,
letterboxOffset: number,
logoDetected: [boolean, boolean, boolean, boolean]
aspectRatioInvalid: boolean
aspectRatioUncertainReason?: string
aspectRatioInvalidReason?: string
}
export function initAardTestResults(settings: AardSettings): AardTestResults {
@ -81,7 +83,8 @@ export function initAardTestResults(settings: AardSettings): AardTestResults {
activeAspectRatio: 0,
letterboxWidth: 0,
letterboxOffset: 0,
logoDetected: [false, false, false, false]
logoDetected: [false, false, false, false],
aspectRatioInvalid: false,
}
}
@ -120,4 +123,5 @@ export function resetAardTestResults(results: AardTestResults): void {
results.aspectRatioUncertainReason = null;
results.topRowUncertain = false;
results.bottomRowUncertain = false;
results.aspectRatioInvalid = false;
}

View File

@ -1,8 +1,7 @@
import Debug from '../../conf/Debug';
import BrowserDetect from '../../conf/BrowserDetect';
import Logger from '../Logger';
import Settings from '../Settings';
import EventBus, { EventBusContext } from '../EventBus';
import { ComponentLogger } from '../logging/ComponentLogger';
import { LogAggregator } from '../logging/LogAggregator';
if (process.env.CHANNEL !== 'stable'){
console.info("Loading CommsClient");
@ -73,7 +72,7 @@ class CommsClient {
name: string;
origin: CommsOrigin;
logger: Logger;
logger: ComponentLogger;
settings: any; // sus?
eventBus: EventBus;
@ -82,10 +81,10 @@ class CommsClient {
port: chrome.runtime.Port;
//#region lifecycle
constructor(name: string, logger: Logger, eventBus: EventBus) {
constructor(name: string, logAggregator: LogAggregator, eventBus: EventBus) {
this.name = name;
try {
this.logger = logger;
this.logger = new ComponentLogger(logAggregator, 'CommsClient', {});
this.eventBus = eventBus;
if (name === 'popup-port') {
@ -101,16 +100,16 @@ class CommsClient {
this.port = chrome.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.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);
@ -118,7 +117,7 @@ class CommsClient {
this.commsId = (Math.random() * 20).toFixed(0);
} catch (e) {
console.error("CONSTRUCOTR FAILED:", e)
console.error("CONSTRUCTOR FAILED:", e)
}
}
@ -129,7 +128,9 @@ class CommsClient {
}
//#endregion
async sendMessage(message, context?: EventBusContext){
async sendMessage(message, context?: EventBusContext, borderCrossings?){
this.logger.info('sendMessage', ' <<< Sending message to background script:', message);
message = JSON.parse(JSON.stringify(message)); // vue quirk. We should really use vue store instead
// content script client and popup client differ in this one thing
@ -143,9 +144,12 @@ class CommsClient {
return port.postMessage(message);
}
}
// send to server
if (!context?.borderCrossings?.commsServer) {
return chrome.runtime.sendMessage(null, message, null);
}
}
/**
* Processes message we received from CommsServer, and forwards it to eventBus.
@ -169,7 +173,10 @@ class CommsClient {
message.config,
{
comms,
origin: CommsOrigin.Server
origin: CommsOrigin.Server,
borderCrossings: {
commsServer: true
}
}
);
}

View File

@ -1,16 +1,16 @@
import { EventBusContext } from './../EventBus';
import Debug from '../../conf/Debug';
import BrowserDetect from '../../conf/BrowserDetect';
import Logger from '../Logger';
import { ComponentLogger } from './../logging/ComponentLogger';
import Settings from '../Settings';
import ExtensionMode from '../../../common/enums/ExtensionMode.enum';
import EventBus from '../EventBus';
import { CommsOrigin } from './CommsClient';
const BASE_LOGGING_STYLES = {
log: "background-color: #11D; color: #aad",
};
class CommsServer {
server: any;
logger: Logger;
logger: ComponentLogger;
settings: Settings;
eventBus: EventBus;
@ -52,16 +52,33 @@ class CommsServer {
} = {};
popupPort: any;
private _lastActiveTab: chrome.tabs.Tab | undefined;
//#region getters
get activeTab() {
return chrome.tabs.query({currentWindow: true, active: true});
get activeTab(): Promise<chrome.tabs.Tab | undefined> {
return new Promise((resolve, reject) => {
chrome.tabs
.query({currentWindow: true, active: true})
.then((tabs) => {
if (tabs.length === 0) {
this.logger.warn('<getter-activeTab>', 'no active tab found, returning last valid active tab instead ...', this._lastActiveTab);
resolve(this._lastActiveTab);
} else {
this.logger.log('<getter-activeTab>', 'getting active tab', tabs[0]);
this._lastActiveTab = tabs[0];
resolve(tabs[0]);
}
})
.catch((err) => {
this.logger.error('<getter-activeTab>', 'error while getting active tab — returned last valid active tab instead ...', err, this._lastActiveTab);
});
});
}
//#endregion
//#region lifecycle
constructor(server) {
this.server = server;
this.logger = server.logger;
this.logger = new ComponentLogger(server.logAggregator, 'CommsServer', {styles: BASE_LOGGING_STYLES});
this.settings = server.settings;
this.eventBus = server.eventBus;
@ -105,10 +122,45 @@ class CommsServer {
//#endregion
/**
* Lists all unique hosts that are present in all the frames of a given tab.
* This includes both hostname of the tab, as well as of all iframes embedded in it.
* @returns
*/
async listUniqueFrameHosts() {
const aTab = await this.activeTab;
const tabPort = this.ports[aTab.id];
const hosts = [];
for (const frame in tabPort) {
for (const portName in tabPort[frame]) {
const port = tabPort[frame][portName];
const host = port.sender.origin.split('://')[1];
// if host is invalid or already exists in our list, skip adding it
if (!host || hosts.includes(host)) {
continue;
}
hosts.push(host);
}
}
console.log('uniq hosts:', hosts)
return hosts;
}
sendMessage(message, context?) {
this.logger.debug('sendMessage', `preparing to send message ${message.command ?? ''} ...`, {message, context});
// stop messages from returning where they came from, and prevent
// cross-pollination between content scripts running in different
// tabs.
if (!context) {
this.logger.debug('sendMessage', 'context was not passed in as parameter - does message have context?', message.context);
context = message.context;
}
if (context?.origin !== CommsOrigin.ContentScript) {
if (context?.comms.forwardTo === 'all') {
@ -129,11 +181,16 @@ class CommsServer {
// okay I lied! Messages originating from content script can be forwarded to
// content scripts running in _other_ frames of the tab
let forwarded = false;
if (context?.origin === CommsOrigin.ContentScript) {
if (context?.comms.forwardTo === 'all-frames') {
forwarded = true;
this.sendToOtherFrames(message, context);
}
}
if (!forwarded) {
this.logger.warn('sendMessage', `message ${message.command ?? ''} was not forwarded to any destination!`, {message, context});
}
}
/**
@ -148,10 +205,13 @@ class CommsServer {
* Does NOT send a message to popup.
**/
private sendToAll(message){
this.logger.info('sendToAll', "sending message to all content scripts", message);
for(const tid in this.ports){
const tab = this.ports[tid];
for(const frame in tab){
for (const port in tab[frame]) {
this.logger.info('sendToAll', ` <——— attempting to send message ${message.command ?? ''} to tab ${tab}, frame ${frame}, port ${port}`, message);
tab[frame][port].postMessage(message);
}
}
@ -168,9 +228,11 @@ class CommsServer {
private async sendToFrameContentScripts(message, tab, frame, port?) {
if (port !== undefined) {
this.ports[tab][frame][port].postMessage(message);
this.logger.info('sendToOtherFrames', ` <——— attempting to send message ${message.command ?? ''} to tab ${tab}, frame ${frame}, port ${port}`, message);
return;
}
for (const framePort in this.ports[tab][frame]) {
this.logger.info('sendToOtherFrames', ` <——— attempting to send message ${message.command ?? ''} to tab ${tab}, frame ${frame}`, message);
this.ports[tab][frame][framePort].postMessage(JSON.parse(JSON.stringify(message)));
}
}
@ -198,7 +260,7 @@ class CommsServer {
}
private 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);
this.logger.info('sendToFrame', ` <——— attempting to send message ${message.command ?? ''} to tab ${tab}, frame ${frame}`, message);
if (isNaN(tab)) {
if (frame === '__playing') {
@ -212,33 +274,32 @@ class CommsServer {
[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);
this.logger.info('sendToFrame', ` <——— attempting to send message ${message.command ?? ''} to tab ${tab}, frame ${frame}`, 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);
this.logger.error('sendToFrame', ` Sending message failed. Reason:`, e);
}
}
private 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);
this.logger.info('sendToActive', ` <——— trying to send a message ${message.command ?? ''} to active tab. Message:`, message);
const tabs = await this.activeTab;
const tab = 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]);
}
this.logger.info('sendToActive', "currently active tab?", tab);
for (const frame in this.ports[tabs[0].id]) {
this.sendToFrameContentScripts(message, tabs[0].id, frame);
for (const frame in this.ports[tab.id]) {
this.logger.info('sendToActive', "sending message to frame:", frame, this.ports[tab.id][frame], '; message:', message);
this.sendToFrameContentScripts(message, tab.id, frame);
}
}
private async processReceivedMessage(message, port, sender?: {frameId: string, tabId: string}){
this.logger.info('processMessage', ` ==> Received message ${message.command ?? ''} from content script or port`, "background-color: #11D; color: #aad", message, port, sender);
// this triggers events
this.eventBus.send(
message.command,
@ -259,7 +320,7 @@ class CommsServer {
}
private 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.logger.info('processMessage_nonpersistent', ` ==> Received message from background script!`, message, sender);
this.eventBus.send(
message.command,

View File

@ -1,11 +1,11 @@
import EventBus, { EventBusCommand } from '../EventBus';
import Logger from '../Logger';
import { ComponentLogger } from '../logging/ComponentLogger';
import Settings from '../Settings';
import { SiteSettings } from '../settings/SiteSettings';
export class KbmBase {
listenFor: string[] = [];
logger: Logger;
logger: ComponentLogger;
settings: Settings;
siteSettings: SiteSettings;
eventBus: EventBus;
@ -28,7 +28,7 @@ export class KbmBase {
},
}
constructor(eventBus: EventBus, siteSettings: SiteSettings, settings: Settings, logger: Logger) {
constructor(eventBus: EventBus, siteSettings: SiteSettings, settings: Settings, logger: ComponentLogger) {
this.logger = logger;
this.settings = settings;
this.eventBus = eventBus;

View File

@ -1,18 +1,19 @@
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';
import EventBus, { EventBusCommand } from '../EventBus';
import KbmBase from './KbmBase';
import { SiteSettings } from '../settings/SiteSettings';
import { LogAggregator } from '../logging/LogAggregator';
import { ComponentLogger } from '../logging/ComponentLogger';
if(process.env.CHANNEL !== 'stable'){
console.info("Loading KeyboardHandler");
}
const BASE_LOGGING_STYLES = {
log: "color: #ff0"
};
/**
* Handles keypresses and mouse movement.
*
@ -23,7 +24,6 @@ if(process.env.CHANNEL !== 'stable'){
*/
export class KeyboardHandler extends KbmBase {
listenFor: string[] = ['keyup'];
logger: Logger;
settings: Settings;
siteSettings: SiteSettings;
eventBus: EventBus;
@ -45,14 +45,18 @@ export class KeyboardHandler extends KbmBase {
}
//#region lifecycle
constructor(eventBus: EventBus, siteSettings: SiteSettings, settings: Settings, logger: Logger) {
super(eventBus, siteSettings, settings, logger);
constructor(eventBus: EventBus, siteSettings: SiteSettings, settings: Settings, logAggregator: LogAggregator) {
const tmpLogger = new ComponentLogger(logAggregator, 'KeyboardHandler', {styles: BASE_LOGGING_STYLES});
super(eventBus, siteSettings, settings, tmpLogger);
this.init();
}
init() {
this.logger.log('info', 'debug', "[KeyboardHandler::init] starting init");
this.logger.debug("init", "starting init");
// reset keypressActions when re-initializing, otherwise keypressActions will
// multiply in an unwanted way
this.keypressActions = [];
// build the action list — but only from actions that have shortcuts assigned
for (const key in this.settings.active.commands) {
@ -115,28 +119,28 @@ export class KeyboardHandler extends KbmBase {
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
// 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', "[KeyboardHandler::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
);
}
// this.logger.log('info', 'keyboard', "[KeyboardHandler::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;
@ -209,29 +213,24 @@ export class KeyboardHandler extends KbmBase {
handleKeyup(event) {
// if (!this.keyboardEnabled) {
// this.logger.log('info', 'keyboard', "%c[KeyboardHandler::handleKeyup] kbmHandler.keyboardEnabled is set to false. Doing nothing.");
// return;
// }
this.logger.log('info', 'keyboard', "%c[KeyboardHandler::handleKeyup] we pressed a key: ", "color: #ff0", event.key , " | keyup: ", event.keyup, "event:", event);
this.logger.info('handleKeyup', "we pressed a key: ", event.key , " | keyup: ", event.keyup, "event:", event);
try {
if (this.preventAction(event)) {
this.logger.log('info', 'keyboard', "[KeyboardHandler::handleKeyup] we are in a text box or something. Doing nothing.");
this.logger.info('handleKeyup', "we are in a text box or something. Doing nothing.");
return;
}
this.logger.log('info', 'keyboard', "%c[KeyboardHandler::handleKeyup] Trying to find and execute action for event. Actions/event: ", "color: #ff0", this.keypressActions, event);
this.logger.info('handleKeyup', "Trying to find and execute action for event. Actions/event:", this.keypressActions, event);
const isLatin = this.isLatin(event.key);
for (const command of this.keypressActions) {
if (this.isActionMatch(command.shortcut, event, isLatin)) {
this.eventBus.send(command.action, command.arguments);
}
}
} catch (e) {
this.logger.log('info', 'debug', '[KeyboardHandler::handleKeyup] Failed to handle keyup!', e);
this.logger.debug('handleKeyup', 'Failed to handle keyup!', e);
}
}

View File

@ -1,5 +1,6 @@
import { LogAggregator } from './../logging/LogAggregator';
import EventBus, { EventBusCommand } from '../EventBus';
import Logger from '../Logger';
import { ComponentLogger } from '../logging/ComponentLogger';
import Settings from '../Settings';
import { SiteSettings } from '../settings/SiteSettings';
import KbmBase from './KbmBase';
@ -9,6 +10,10 @@ if(process.env.CHANNEL !== 'stable'){
}
const BASE_LOGGING_STYLES = {
log: "color: #ff0"
};
/**
* Handles keypress
*/
@ -36,21 +41,21 @@ export class MouseHandler extends KbmBase {
}
//#region lifecycle
constructor(playerElement: HTMLElement, eventBus: EventBus, siteSettings: SiteSettings, settings: Settings, logger: Logger) {
super(eventBus, siteSettings, settings, logger);
constructor(playerElement: HTMLElement, eventBus: EventBus, siteSettings: SiteSettings, settings: Settings, logAggregator: LogAggregator) {
const tmpLogger = new ComponentLogger(logAggregator, 'MouseHandler', {styles: BASE_LOGGING_STYLES});
super(eventBus, siteSettings, settings, tmpLogger);
this.logger = logger;
this.settings = settings;
this.siteSettings = siteSettings;
this.eventBus = eventBus;
this.playerElement = playerElement;
this.init();
}
init() {
this.logger.log('info', 'debug', '[MouseHandler::init] starting init');
// this.logger.debug('init', 'starting init');
}
load() {

View File

@ -0,0 +1,74 @@
import { LogAggregator, LogSourceOptions } from './LogAggregator';
export enum LogLevel {
Debug = 'debug',
Info = 'info',
Log = 'log',
Warn = 'warn',
Error = 'error',
}
export type ComponentLoggerOptions = {
styles?: {
[x in LogLevel]?: string;
}
}
export class ComponentLogger {
private logAggregator: LogAggregator;
private component: string;
private componentOptions?: ComponentLoggerOptions;
constructor(logAggregator: LogAggregator, component: string, componentOptions?: ComponentLoggerOptions) {
this.logAggregator = logAggregator;
this.component = component;
this.componentOptions = componentOptions;
}
private handleLog(logLevel: LogLevel, sourceFunction: string | LogSourceOptions, ...message: any) {
let functionSource = typeof sourceFunction === 'string' ? sourceFunction : sourceFunction?.src;
let consoleMessageString = `[${this.component}${functionSource ? `::${functionSource}` : ''}]`;
const consoleMessageData = []
for (const m of message) {
if (typeof m === 'string') {
consoleMessageString = `${consoleMessageString} ${m}`;
} else if (typeof m === 'number') {
consoleMessageString = `${consoleMessageString} %f`;
consoleMessageData.unshift(m);
} else if (typeof HTMLElement !== 'undefined' && m instanceof HTMLElement) { // HTMLElement does not exist in background script, but this class may
consoleMessageString = `${consoleMessageString} %o`;
consoleMessageData.unshift(m);
} else {
consoleMessageString = `${consoleMessageString} %O`;
consoleMessageData.unshift(m);
}
}
const style = this.componentOptions?.styles?.[logLevel] ?? this.componentOptions?.styles?.[LogLevel.Log];
if (style) {
consoleMessageString = `%c${consoleMessageString}`;
consoleMessageData.unshift(style);
}
this.logAggregator.log(this.component, logLevel, typeof sourceFunction === 'object' ? sourceFunction : undefined, consoleMessageString, ...consoleMessageData);
}
debug(sourceFunction: string | LogSourceOptions, ...message: any[]) {
this.handleLog(LogLevel.Debug, sourceFunction, ...message);
}
info(sourceFunction: string | LogSourceOptions, ...message: any[]) {
this.handleLog(LogLevel.Info, sourceFunction, ...message);
}
log(sourceFunction: string | LogSourceOptions, ...message: any[]) {
this.handleLog(LogLevel.Log, sourceFunction, ...message);
}
warn(sourceFunction: string | LogSourceOptions, ...message: any[]) {
this.handleLog(LogLevel.Warn, sourceFunction, ...message);
}
error(sourceFunction: string | LogSourceOptions, ...message: any[]) {
this.handleLog(LogLevel.Error, sourceFunction, ...message);
}
}

View File

@ -0,0 +1,232 @@
import { log } from 'console';
export const BLANK_LOGGER_CONFIG: LogConfig = {
logToConsole: false,
logToFile: false,
component: {
},
origins: {
videoRescan: { disabled: true},
}
}
export interface LogSourceOptions {
src?: string;
origin?: string;
}
export interface LogComponentConfig {
enabled: boolean;
}
export interface LogConfig {
logToConsole?: boolean;
logToFile?: boolean;
stopAfter?: number;
component?: {[x: string]: LogComponentConfig};
origins?: {
videoRescan?: {
disabled: boolean;
}
}
}
const STORAGE_LOG_SETTINGS_KEY = 'uw-log-config';
export class LogAggregator {
private segment: string;
private config: LogConfig;
private startTime: number;
history: any[];
static async getConfig() {
let ret = await chrome.storage.local.get(STORAGE_LOG_SETTINGS_KEY);
if (process.env.CHANNEL === 'dev') {
try {
console.info("[LogAggregator::getSaved] Got settings:", JSON.parse(ret[STORAGE_LOG_SETTINGS_KEY]));
} catch (e) {
console.info("[LogAggregator::getSaved] No settings.", ret)
}
}
try {
return JSON.parse(ret[STORAGE_LOG_SETTINGS_KEY]);
} catch (e) {
return JSON.parse(JSON.stringify(BLANK_LOGGER_CONFIG));
}
}
static async saveConfig(conf: LogConfig) {
try {
const confCp = JSON.parse(JSON.stringify(conf));
if (process.env.CHANNEL === 'dev') {
console.info('Saving logger conf:', confCp)
}
await chrome.storage.local.set({[STORAGE_LOG_SETTINGS_KEY]: JSON.stringify(confCp)} );
} catch (e) {
console.warn('[LogAggregator::saveConfig] Error while trying to save logger config:', e);
}
}
static syncConfig(callback: (x) => void) {
chrome.storage.onChanged.addListener( (changes, area) => {
if (changes.uwLogger) {
const newLoggerConf = JSON.parse(changes.uwLogger.newValue)
if (process.env.CHANNEL === 'dev') {
console.info('Logger settings reloaded. New conf:', newLoggerConf);
}
callback(newLoggerConf);
}
});
}
constructor(segment: string) {
this.segment = segment;
chrome.storage.onChanged.addListener((changes, area) => {
this.storageChangeListener(changes, area)
});
this.init();
}
private canLog(component: string, sourceOptions?: LogSourceOptions): boolean {
// component is not in config, so we add a blank entry
if (this.config && !this.config.component[component]) {
this.config.component[component] = {enabled: false};
LogAggregator.saveConfig(this.config);
return false;
}
if (this.config?.component?.[component]?.enabled) {
if (sourceOptions?.origin && this.config?.origins?.[sourceOptions.origin]?.disabled) {
return false;
}
return true;
}
return false;
}
private storageChangeListener(changes, area) {
if (!changes[STORAGE_LOG_SETTINGS_KEY]) {
console.info('We dont have any logging settings, not processing frther', changes);
return;
}
try {
this.config = JSON.parse(changes[STORAGE_LOG_SETTINGS_KEY].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.config, '\nraw changes:', changes, 'area?', area,
'\n————————————————————————————————————————————————————————————————————————\n\n\n\n\n\n\n\n\n\n\n\n-----\nLogging with new settings starts now.'
);
this.init(this.config);
}
private parseStack() {
const trace = (new Error()).stack;
const stackInfo: any = {};
// we turn our stack into array and remove the "file::line" part of the trace,
// since that is useless because minification/webpack
stackInfo['stack'] = {trace: trace.split('\n').map(a => a.split('@')[0])};
// here's possible sources that led to this log entry
stackInfo['periodicPlayerCheck'] = false;
stackInfo['periodicVideoStyleChangeCheck'] = false;
stackInfo['aard'] = false;
stackInfo['keyboard'] = false;
stackInfo['popup'] = false;
stackInfo['mousemove'] = false;
stackInfo['exitLogs'] = false;
// here we check which source triggered the action. There can be more
// than one source, too, so we don't break when we find the first one
for (const line of stackInfo.stack.trace) {
if (line === 'doPeriodicPlayerElementChangeCheck') {
stackInfo['periodicPlayerCheck'] = true;
} else if (line === 'doPeriodicFallbackChangeDetectionCheck') {
stackInfo['periodicVideoStyleChangeCheck'] = true;
} else if (line === 'frameCheck') {
stackInfo['aard'] = true;
} else if (line === 'execAction') {
stackInfo['keyboard'] = true;
} else if (line === 'processReceivedMessage') {
stackInfo['popup'] = true;
} else if (line === 'handleMouseMove') {
stackInfo['mousemove'] = true;
}
}
// exitLog overrides any other exclusions, so we look for it separately.
// we also remove some of the unnecessary messages to reduce log file size
for(let i = 0; i < stackInfo.stack.trace.length; i++) {
if (stackInfo.stack.trace[i] === 'finish') {
stackInfo['exitLogs'] = true;
break;
}
// if we hit one of these, we remove the rest of the array and call it a
// day. Chances are there's nothing of value past this point.
if (stackInfo.stack.trace[i].indexOf('promise callback') !== -1
|| stackInfo.stack.trace[i].indexOf('asyncGeneratorStep') !== -1
|| stackInfo.stack.trace[i].indexOf('_asyncToGenerator') !== -1
|| stackInfo.stack.trace[i].startsWith('_next')) {
stackInfo.stack.trace.splice(i);
break;
}
}
return stackInfo;
}
// TODO: implement this
private stopLogging() {
}
async init(config?: LogConfig) {
if (!config) {
config = await LogAggregator.getConfig();
}
this.config = config;
this.startTime = performance.now();
if (this.config?.stopAfter) {
setTimeout(
() => {
this.stopLogging();
},
this.config.stopAfter * 1000
);
}
}
log(component: string, logLevel: string, sourceOptions: LogSourceOptions, message: string, ...data: any[]) {
if (! this.canLog(component, sourceOptions)) {
return;
}
if (this.config.logToFile) {
}
if (this.config.logToConsole){
console[logLevel](`[${this.segment}]>>${message}`, ...data, {stack: this.parseStack()});
}
}
}

View File

@ -0,0 +1,21 @@
export default interface LogConfig {
outputs: {
console: boolean,
buffer: boolean,
},
components: {
settings?: boolean,
aard?: boolean,
videoData?: boolean,
resizer?: boolean,
comms?: boolean,
},
environments: {
page: boolean,
popup: boolean,
ui: boolean,
uwServer: boolean,
}
}

View File

@ -0,0 +1,10 @@
export interface LogMessageOrigin {
component: string,
environment: string,
}
export interface LogMessage {
time: Date;
message: any,
origin: LogMessageOrigin
}

View File

@ -0,0 +1,107 @@
import { settings } from 'cluster'
import SettingsInterface from '@src/common/interfaces/SettingsInterface';
export interface SettingsSnapshot {
isAutomatic?: boolean;
isProtected?: boolean;
isDefault?: boolean;
forVersion: string;
label: string;
settings: SettingsInterface;
createdAt: Date;
}
export interface SettingsSnapshotOptions {
isAutomatic?: boolean,
isProtected?: boolean,
isDefault?: boolean,
label?: string,
forVersion?: string
}
export class SettingsSnapshotManager {
private MAX_AUTOMATIC_SNAPSHOTS = 5;
async getSnapshot(index?: number) {
const snapshots = await this.listSnapshots();
if (!index) {
return snapshots.find(x => x.isDefault);
} else {
if (index < 0 || index >= snapshots.length) {
throw new Error('Invalid index');
}
return snapshots[index];
}
}
async createSnapshot(settings: SettingsInterface, options?: SettingsSnapshotOptions) {
const snapshot = {
...options,
label: options.label ?? 'Automatic snapshot',
forVersion: options.forVersion || settings.version,
settings: JSON.parse(JSON.stringify(settings)),
createdAt: new Date(),
};
const snapshots = await this.listSnapshots();
const automaticSnapshots = snapshots.filter((s) => s.isAutomatic && !s.isProtected);
if (options.isAutomatic && automaticSnapshots.length >= this.MAX_AUTOMATIC_SNAPSHOTS) {
const firstAutomaticIndex = snapshots.findIndex((s) => s.isAutomatic && !s.isProtected);
snapshots.splice(firstAutomaticIndex, 1);
}
snapshots.push(snapshot);
this.set(snapshots);
}
async setDefaultSnapshot(index: number, isDefault: boolean) {
const snapshots = await this.listSnapshots();
if (index < 0 || index >= snapshots.length) {
throw new Error('Invalid index');
}
if (isDefault) {
for (const snapshot of snapshots) {
snapshot.isDefault = false;
}
}
snapshots[index].isDefault = isDefault;
this.set(snapshots);
}
async markSnapshotAsProtected(index: number, isProtected: boolean) {
const snapshots = await this.listSnapshots();
if (index < 0 || index >= snapshots.length) {
throw new Error('Invalid index');
}
snapshots[index].isProtected = isProtected;
this.set(snapshots);
}
async deleteSnapshot(index: number) {
const snapshots = await this.listSnapshots();
if (index < 0 || index >= snapshots.length) {
throw new Error('Invalid index');
}
snapshots.splice(index, 1);
this.set(snapshots);
}
async listSnapshots(): Promise<SettingsSnapshot[]> {
const ret = await chrome.storage.local.get('uwSettings-snapshots');
try {
const json = JSON.parse(ret['uwSettings-snapshots']) as SettingsSnapshot[];
return json;
} catch (e) {
return [] as SettingsSnapshot[];
}
}
private async set(snapshots: SettingsSnapshot[] = []) {
await chrome.storage.local.set({
'uwSettings-snapshots': JSON.stringify(snapshots),
});
}
}

View File

@ -15,10 +15,17 @@ import VideoAlignmentType from '../../../common/enums/VideoAlignmentType.enum';
*/
export class SiteSettings {
private settings: Settings;
private site: string;
private _site: string;
private set site(x: string) {
this._site = x;
}
public get site() {
return this._site;
}
raw: SiteSettingsInterface; // actual settings
data: SiteSettingsInterface; // effective settings
usesSettingsFor: string | undefined;
temporaryData: SiteSettingsInterface;
sessionData: SiteSettingsInterface;
readonly defaultSettings: SiteSettingsInterface;
@ -30,7 +37,7 @@ export class SiteSettings {
this.settings = settings;
this.raw = settings.active.sites[site];
this.site = site;
this.defaultSettings = settings.default.sites['@global'];
this.defaultSettings = settings.active.sites['@global'];
this.compileSettingsObject();
@ -47,12 +54,63 @@ export class SiteSettings {
chrome.storage.onChanged.addListener((changes, area) => {this.storageChangeListener(changes, area)})
}
/**
* Tries to match websites, even if we're on a different subdomain.
* @returns
*/
private getSettingsForSite() {
if (!this.site) {
return {
siteSettings: this.settings.active.sites['@global'],
usesSettingsFor: '@global'
};
}
if (this.settings.active.sites[this.site]) {
return {
siteSettings: this.settings.active.sites[this.site],
usesSettingsFor: undefined
};
}
const urlSegments = this.site.split('.').reverse();
siteLoop:
for (const cs in this.settings.active.sites) {
const configUrlSegments = cs.split('.').reverse();
// Match site with wildcard site definitions
// Also, if definition starts with 'www', match also other subdomains — e.g. if we have a configuration for
// `www.example.com`, this will also match `example.com`, `subdomain.example.com`, `nested.subdomain.example.com` ...
if (configUrlSegments[configUrlSegments.length - 1] === '*' || (configUrlSegments[configUrlSegments.length - 1] === 'www')) {
console.log('ss: comparing', configUrlSegments, urlSegments);
for (let i = 0; i < configUrlSegments.length - 1 && i < urlSegments.length; i++) {
if (configUrlSegments[i] !== urlSegments[i]) {
continue siteLoop;
}
}
return {
siteSettings: this.settings.active.sites[cs],
usesSettingsFor: cs
}
}
}
return {
siteSettings: this.settings.active.sites['@global'],
usesSettingsFor: '@global'
};
}
/**
* Merges defaultSettings into site settings where appropriate.
* Alan pls ensure default settings object follows the correct structure
*/
private compileSettingsObject() {
this.data = _cp(this.settings.active.sites[this.site] ?? {})
const {siteSettings, usesSettingsFor} = this.getSettingsForSite();
this.data = _cp(siteSettings);
this.usesSettingsFor = usesSettingsFor;
if (!this.data) {
this.data = _cp(this.defaultSettings);
@ -150,8 +208,8 @@ export class SiteSettings {
// we aren't stepping on any other toes by doing this, since everyone
// gets the first change
this.settings.active._updateFlags = undefined;
this.settings.saveWithoutReload();
// this.settings.active._updateFlags = undefined;
// this.settings.saveWithoutReload();
}
}
@ -317,7 +375,8 @@ export class SiteSettings {
this.settings.active.sites[this.site].type = 'unknown';
} else {
if (!this.settings.active.sites[this.site] || this.settings.active.sites[this.site].autocreated) {
this.settings.active.sites[this.site] = _cp(this.settings.active.sites['@empty']);
this.settings.active.sites[this.site] = _cp(this.data);
this.settings.active.sites[this.site].type = 'user-defined';
}
}

View File

@ -1,3 +1,4 @@
import ExtensionMode from '../../../common/enums/ExtensionMode.enum';
import { EventBusConnector } from '../EventBus';
if (process.env.CHANNEL !== 'stable'){
@ -22,6 +23,7 @@ class UI {
this.lastProbeResponseTs = null;
this.isGlobal = uiConfig.isGlobal ?? false;
this.isIframe = window.self !== window.top;
this.eventBus = uiConfig.eventBus;
this.disablePointerEvents = false;
@ -29,24 +31,48 @@ class UI {
this.saveState = undefined;
this.playerData = uiConfig.playerData;
this.uiSettings = uiConfig.uiSettings;
this.siteSettings = uiConfig.siteSettings;
this.iframeErrorCount = 0;
this.iframeConfirmed = false;
this.iframeRejected = false;
this.delayedDestroyTimer = null;
// TODO: at some point, UI should be different for global popup and in-player UI
this.csuiScheme = this.getCsuiScheme();
const csuiVersion = this.getCsuiVersion(this.csuiScheme.contentScheme);
this.uiURI = chrome.runtime.getURL(`/csui/${csuiVersion}.html`);
this.extensionBase = chrome.runtime.getURL('').replace(/\/$/, "");
// UI will be initialized when setUiVisibility is called
console.log('ui config:', uiConfig);
this.init();
}
canRun() {
if (this.isGlobal) {
return true;
}
return this.siteSettings?.data.enableUI.fullscreen === ExtensionMode.Enabled
|| this.siteSettings?.data.enableUI.theater === ExtensionMode.Enabled
|| this.siteSettings?.data.enableUI.normal === ExtensionMode.Enabled;
}
async init() {
this.initIframes();
if (!this.canRun()) {
console.log('ui config: canRun returned false', this.siteSettings?.data.enableUI.fullscreen === ExtensionMode.Enabled, this.siteSettings?.data.enableUI.theater === ExtensionMode.Enabled, this.siteSettings?.data.enableUI.normal === ExtensionMode.Enabled)
return;
}
console.log('ui config: canRun returned truie', this.siteSettings?.data.enableUI.fullscreen === ExtensionMode.Enabled, this.siteSettings?.data.enableUI.theater === ExtensionMode.Enabled, this.siteSettings?.data.enableUI.normal === ExtensionMode.Enabled)
this.initUIContainer();
this.loadIframe();
this.initMessaging();
}
/**
* Returns color scheme we need to use.
*
@ -70,7 +96,7 @@ class UI {
return csuiVersions[preferredScheme] ?? csuiVersions.normal;
}
initIframes() {
initUIContainer() {
const random = Math.round(Math.random() * 69420);
const uwid = `uw-ultrawidify-${this.interfaceId}-root-${random}`
@ -81,8 +107,8 @@ class UI {
}
rootDiv.setAttribute('id', uwid);
rootDiv.classList.add('uw-ultrawidify-container-root');
// rootDiv.style.width = "100%";
// rootDiv.style.height = "100%";
rootDiv.style.width = "100%";
rootDiv.style.height = "100%";
rootDiv.style.position = this.isGlobal ? "fixed" : "absolute";
rootDiv.style.zIndex = this.isGlobal ? '90009' : '90000';
rootDiv.style.border = 0;
@ -95,8 +121,10 @@ class UI {
document.body.appendChild(rootDiv);
}
this.element = rootDiv;
this.rootDiv = rootDiv;
}
loadIframe() {
// in onMouseMove, we currently can't access this because we didn't
// do things the most properly
const uiURI = this.uiURI;
@ -132,7 +160,7 @@ class UI {
this.uiIframe = iframe;
// set not visible by default
this.setUiVisibility(false);
// this.setUiVisibility(false);
const fn = (event) => {
// remove self on fucky wuckies
@ -178,14 +206,28 @@ class UI {
document.addEventListener('mousemove', fn, true);
}
rootDiv.appendChild(iframe);
this.eventBus.forwardToIframe(
this.uiIframe,
(action, payload) => {
this.sendToIframe(action, payload, {})
}
);
this.rootDiv.appendChild(iframe);
}
unloadIframe() {
this.eventBus.cancelIframeForwarding(this.uiIframe);
window.removeEventListener('message', this.messageHandlerFn);
this.uiIframe?.remove();
delete this.uiIframe;
}
initMessaging() {
// subscribe to events coming back to us. Unsubscribe if iframe vanishes.
window.addEventListener('message', this.messageHandlerFn);
/* set up event bus tunnel from content script to UI necessary if we want to receive
* like current zoom levels & current aspect ratio & stuff. Some of these things are
* necessary for UI display in the popup.
@ -200,7 +242,7 @@ class UI {
},
'uw-set-ui-state': {
function: (config, routingData) => {
if (config.globalUiVisible !== undefined) {
if (config.globalUiVisible !== undefined && !this.isIframe) {
if (this.isGlobal) {
this.setUiVisibility(config.globalUiVisible);
} else {
@ -250,28 +292,40 @@ class UI {
}
setUiVisibility(visible) {
// console.log('uwui - setting ui visibility!', visible, this.isGlobal ? 'global' : 'page', this.uiIframe, this.rootDiv);
// if (!this.uiIframe || !this.rootDiv) {
// this.init();
// }
if (visible) {
this.element.style.width = '100%';
this.element.style.height = '100%';
this.rootDiv.style.width = '100%';
this.rootDiv.style.height = '100%';
this.uiIframe.style.width = '100%';
this.uiIframe.style.height = '100%';
// if (this.delayedDestroyTimer) {
// clearTimeout(this.delayedDestroyTimer);
// }
} else {
this.element.style.width = '0px';
this.element.style.height = '0px';
this.rootDiv.style.width = '0px';
this.rootDiv.style.height = '0px';
this.uiIframe.style.width = '0px';
this.uiIframe.style.height = '0px';
// destroy after 30 seconds of UI being hidden
// this.delayedDestroyTimer = setTimeout( () => this.unloadIframe(), 30000);
}
}
async enable() {
// if root element is not present, we need to init the UI.
if (!this.element) {
if (!this.rootDiv) {
await this.init();
}
// otherwise, we don't have to do anything
}
disable() {
if (this.element) {
if (this.rootDiv) {
this.destroy();
}
}
@ -290,6 +344,10 @@ class UI {
canShowUI: false,
}
if (this.playerData?.environment && this.siteSettings.data.enableUI[this.playerData?.environment] !== ExtensionMode.Enabled) {
return result;
}
if (this.playerData?.dimensions) {
result.playerDimensions = this.playerData.dimensions;
}
@ -306,6 +364,7 @@ class UI {
}
result.canShowUI = true;
return result;
}
@ -366,7 +425,17 @@ class UI {
break;
case 'uw-bus-tunnel':
const busCommand = event.data.payload;
this.eventBus.send(busCommand.action, busCommand.config, busCommand.routingData);
this.eventBus.send(
busCommand.action,
busCommand.config,
{
...busCommand?.context,
borderCrossings: {
...busCommand?.context?.borderCrossings,
iframe: true,
}
}
);
break;
case 'uwui-get-role':
this.sendToIframeLowLevel('uwui-set-role', {role: this.isGlobal ? 'global' : 'player'});
@ -398,7 +467,7 @@ class UI {
// because existence of UI is not guaranteed — UI is not shown when extension is inactive.
// If extension is inactive due to "player element isn't big enough to justify it", however,
// we can still receive eventBus messages.
if (this.element && this.uiIframe) {
if (this.rootDiv && this.uiIframe) {
this.uiIframe.contentWindow?.postMessage(
{
action,
@ -445,21 +514,21 @@ class UI {
replace(newUiConfig) {
this.uiConfig = newUiConfig;
if (this.element) {
this.element?.remove();
if (this.rootDiv) {
this.destroy();
this.init();
}
}
destroy() {
window.removeEventListener('message', this.messageHandlerFn);
this.unloadIframe();
this.eventBus.unsubscribeAll(this);
// this.comms?.destroy();
this.uiIframe?.remove();
this.element?.remove();
this.rootDiv?.remove();
this.uiIframe = undefined;
this.element = undefined;
delete this.uiIframe;
delete this.rootDiv;
}
}

View File

@ -1,15 +1,12 @@
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';
import EventBus from '../EventBus';
import { SiteSettings } from '../settings/SiteSettings';
import IframeManager from './IframeManager';
import { LogAggregator } from '../logging/LogAggregator';
import { ComponentLogger } from '../logging/ComponentLogger';
if (process.env.CHANNEL !== 'stable'){
console.info("Loading PageInfo");
@ -52,7 +49,8 @@ class PageInfo {
//#endregion
//#region helper objects
logger: Logger;
logAggregator: LogAggregator;
logger: ComponentLogger;
settings: Settings;
siteSettings: SiteSettings;
comms: CommsClient;
@ -71,6 +69,7 @@ class PageInfo {
keyboardHandler: any;
fsStatus = {fullscreen: true}; // fsStatus needs to be passed to VideoData, so fullScreen property is shared between videoData instances
isIframe: boolean = false;
//#endregion
fsEventListener = {
@ -80,8 +79,11 @@ class PageInfo {
}
};
constructor(eventBus: EventBus, siteSettings: SiteSettings, settings: Settings, logger: Logger, readOnly = false){
this.logger = logger;
constructor(eventBus: EventBus, siteSettings: SiteSettings, settings: Settings, logAggregator: LogAggregator, readOnly = false) {
this.isIframe = window.self !== window.top;
this.logAggregator = logAggregator;
this.logger = new ComponentLogger(logAggregator, 'PageInfo', {});
this.settings = settings;
this.siteSettings = siteSettings;
@ -109,10 +111,19 @@ class PageInfo {
this.scheduleUrlCheck();
document.addEventListener('fullscreenchange', this.fsEventListener);
this.eventBus.subscribeMulti({
'probe-video': {
function: () => {
console.log(`[${window.location}] probe-video received.`)
this.rescan();
}
}
});
}
destroy() {
// this.logger.log('info', ['debug', 'init'], "[PageInfo::destroy] destroying all videos!")
// this.logger.debug('destroy', 'destroying all videos!")
if(this.rescanTimer){
clearTimeout(this.rescanTimer);
}
@ -121,7 +132,7 @@ class PageInfo {
this.eventBus.send('noVideo', undefined);
video.videoData.destroy();
} catch (e) {
this.logger.log('error', ['debug', 'init'], '[PageInfo::destroy] unable to destroy video! Error:', e);
this.logger.error('destroy', 'unable to destroy video! Error:', e);
}
}
@ -179,106 +190,62 @@ class PageInfo {
}
}
getVideos(): HTMLVideoElement[] {
/**
* Returns all videos on the page.
*
* If minSize is provided, it only returns <video> elements that are
* equal or bigger than desired size:
*
* * sm: 320 x 180
* * md: 720 x 400
* * lg: 1280 x 720
*
* If minSize is omitted, it returns all <video> elements.
* @param minSize
* @returns
*/
getAllVideos(minSize?: 'sm' | 'md' | 'lg') {
const videoQs = this.siteSettings.getCustomDOMQuerySelector('video');
let videos: HTMLVideoElement[] = [];
if (videoQs){
videos = Array.from(document.querySelectorAll(videoQs) as NodeListOf<HTMLVideoElement> ?? []);
} else{
} else {
videos = Array.from(document.getElementsByTagName('video') ?? []);
}
// filter out videos that aren't big enough
videos = videos.filter(
(v: HTMLVideoElement) => v.clientHeight > 720 && v.clientWidth > 1208
);
if (!minSize) {
return videos;
}
return this.filterVideos(videos, minSize);
}
filterVideos(videos: HTMLVideoElement[], minSize: 'sm' | 'md' | 'lg') {
// minimums are determined by vibes and shit.
// 'sm' is based on "slightly smaller than embeds on old.reddit"
const minX = { sm: 320, md: 720, lg: 1280 };
const minY = { sm: 180, md: 400, lg: 720 };
// filter out videos that aren't big enough
return videos.filter(
(v: HTMLVideoElement) => v.clientHeight >= minY[minSize] && v.clientWidth >= minX[minSize]
);
}
/**
* Gets videos on the page that are big enough for extension to trigger
* @returns
*/
getVideos(): HTMLVideoElement[] {
return this.getAllVideos('lg');
}
hasVideo() {
return this.readOnly ? this.hasVideos : this.videos.length;
}
/**
* Re-scans the page for videos. Removes any videos that no longer exist from our list
* of videos. Destroys all videoData objects for all the videos that don't have their
* own <video> html element on the page.
* @param rescanReason Why was the rescan triggered. Mostly used for logging.
* @returns
*/
rescan(rescanReason?: RescanReason){
// is there any video data objects that had their HTML elements removed but not yet
// destroyed? We clean that up here.
const orphans = this.videos.filter(x => !document.body.contains(x.element));
for (const orphan of orphans) {
orphan.videoData.destroy();
}
// remove all destroyed videos.
this.videos = this.videos.filter(x => !x.videoData.destroyed);
// add new videos
try{
let vids = this.getVideos();
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;
for (const videoElement of vids) {
// do not re-add videos that we already track:
if (this.videos.find(x => x.element.isEqualNode(videoElement))) {
continue;
}
// 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(!videoElement.offsetWidth || !videoElement.offsetHeight) {
continue;
}
// at this point, we're certain that we found new videos. Let's update some properties:
this.hasVideos = true;
// if PageInfo is marked as "readOnly", we actually aren't adding any videos to anything because
// that's super haram. We're only interested in whether
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;
}
this.logger.log('info', 'videoRescan', "[PageInfo::rescan] found new video candidate:", videoElement, "NOTE:: Video initialization starts here:\n--------------------------------\n")
try {
const newVideo = new VideoData(videoElement, this.settings, this.siteSettings, this);
this.videos.push({videoData: newVideo, element: videoElement});
} 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();
private emitVideoStatus(videosDetected?: boolean) {
// if we're left without videos on the current page, we unregister the page.
// if we have videos, we call register.
if (this.eventBus) {
@ -296,24 +263,110 @@ class PageInfo {
// 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});
if (videosDetected || this.hasVideo()) {
this.eventBus.send('has-video', null);
} else {
// this.comms.unregisterVideo({host: window.location.hostname, location: window.location});
this.eventBus.send('noVideo', null);
}
}
}
/**
* Re-scans the page for videos. Removes any videos that no longer exist from our list
* of videos. Destroys all videoData objects for all the videos that don't have their
* own <video> html element on the page.
* @param rescanReason Why was the rescan triggered. Mostly used for logging.
* @returns
*/
rescan(rescanReason?: RescanReason){
let videosDetected = false;
// is there any video data objects that had their HTML elements removed but not yet
// destroyed? We clean that up here.
const orphans = this.videos.filter(x => !document.body.contains(x.element));
for (const orphan of orphans) {
orphan.videoData.destroy();
}
// remove all destroyed videos.
this.videos = this.videos.filter(x => !x.videoData.destroyed);
// add new videos
try {
// in iframes, emit registerIframe even if video is smaller than required
let vids = this.getAllVideos('sm');
if (this.isIframe && this.eventBus) {
videosDetected ||= vids?.length > 0;
};
// for normal operations, use standard size limits
vids = this.filterVideos(vids, 'lg');
if(!vids || vids.length == 0){
this.hasVideos = false;
if(rescanReason == RescanReason.PERIODIC){
this.logger.info({src: 'rescan', origin: 'videoRescan'}, "Scheduling normal rescan.")
this.scheduleRescan(RescanReason.PERIODIC);
}
this.emitVideoStatus(videosDetected);
return;
}
// add new videos
this.hasVideos = false;
for (const videoElement of vids) {
// do not re-add videos that we already track:
if (this.videos.find(x => x.element.isEqualNode(videoElement))) {
continue;
}
// 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(!videoElement.offsetWidth || !videoElement.offsetHeight) {
continue;
}
// at this point, we're certain that we found new videos. Let's update some properties:
this.hasVideos = true;
videosDetected ||= true;
// if PageInfo is marked as "readOnly", we actually aren't adding any videos to anything because
// that's super haram. We're only interested in whether
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);
}
this.emitVideoStatus(videosDetected);
return;
}
this.logger.info({src: 'rescan', origin: 'videoRescan'}, "found new video candidate:", videoElement, "NOTE:: Video initialization starts here:\n--------------------------------\n")
try {
const newVideo = new VideoData(videoElement, this.settings, this.siteSettings, this);
this.videos.push({videoData: newVideo, element: videoElement});
} catch (e) {
this.logger.error('rescan', "rescan error: failed to initialize videoData. Skipping this video.",e);
}
this.logger.info({src: 'rescan', origin: 'videoRescan'}, "END VIDEO INITIALIZATION\n\n\n-------------------------------------\nvideos[] is now this:", this.videos,"\n\n\n\n\n\n\n\n")
}
this.removeDestroyed();
this.emitVideoStatus(videosDetected);
} catch(e) {
// 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);
this.logger.error('rescan', "rescan error: — destroying all videoData objects",e);
for (const v of this.videos) {
v.videoData.destroy();
}
@ -348,7 +401,7 @@ class PageInfo {
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)
this.logger.error('scheduleRescan', "scheduling rescan failed. Here's why:",e)
}
}
@ -366,13 +419,13 @@ class PageInfo {
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)
this.logger.log('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.logger.warn('ghettoUrlCheck', "URL has changed. Triggering a rescan!");
this.rescan(RescanReason.URL_CHANGE);
this.lastUrl = window.location.href;

View File

@ -1,19 +1,16 @@
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 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';
import EventBus from '../EventBus';
import UI from '../uwui/UI';
import { SiteSettings } from '../settings/SiteSettings';
import PageInfo from './PageInfo';
import { RunLevel } from '../../enum/run-level.enum';
import { ExtensionEnvironment } from '../../../common/interfaces/SettingsInterface';
import { ComponentLogger } from '../logging/ComponentLogger';
if (process.env.CHANNEL !== 'stable'){
console.info("Loading: PlayerData.js");
@ -69,7 +66,7 @@ class PlayerData {
private playerCssClass = 'uw-ultrawidify-player-css';
//#region helper objects
logger: Logger;
logger: ComponentLogger;
videoData: VideoData;
pageInfo: PageInfo;
siteSettings: SiteSettings;
@ -92,10 +89,10 @@ class PlayerData {
isTheaterMode: boolean = false; // note: fullscreen mode will count as theaterMode if player was in theater mode before fs switch. This is desired, so far.
isTooSmall: boolean = true;
//#endregion
//#region misc stuff
extensionMode: any;
dimensions: PlayerDimensions;
private playerIdElement: any;
private observer: ResizeObserver;
private trackChangesTimeout: any;
@ -103,6 +100,8 @@ class PlayerData {
private ui: UI;
private _isTrackDimensionChangesActive: boolean = false;
elementStack: ElementStack = [] as ElementStack;
//#endregion
@ -123,7 +122,7 @@ class PlayerData {
function: (data) => this.markElement(data)
}],
'update-player': [{
function: () => this.getPlayer()
function: () => this.updatePlayer()
}],
'set-run-level': [{
function: (runLevel) => this.setRunLevel(runLevel)
@ -179,11 +178,19 @@ class PlayerData {
return ExtensionEnvironment.Normal;
}
/**
*
* END OF PROPERTIES
*
*
*/
//#region lifecycle
constructor(videoData) {
try {
// set all our helper objects
this.logger = videoData.logger;
this.logger = new ComponentLogger(videoData.logAggregator, 'PlayerData', {styles: {}});
this.videoData = videoData;
this.videoElement = videoData.video;
this.pageInfo = videoData.pageInfo;
@ -192,14 +199,10 @@ class PlayerData {
// do the rest
this.invalid = false;
this.element = this.getPlayer();
this.updatePlayer();
this.isTooSmall = (this.element.clientWidth < 1208 || this.element.clientHeight < 720);
this.initEventBus();
// we defer UI creation until player element is big enough
// this happens in trackDimensionChanges!
this.dimensions = undefined;
this.periodicallyRefreshPlayerElement = false;
@ -215,8 +218,6 @@ class PlayerData {
return;
}
this.trackDimensionChanges();
this.trackEnvironmentChanges();
this.startChangeDetection();
document.addEventListener('fullscreenchange', this.dimensionChangeListener);
@ -231,7 +232,7 @@ class PlayerData {
private reloadPlayerDataConfig(siteConfUpdate) {
// this.siteSettings = siteConfUpdate;
this.element = this.getPlayer();
this.updatePlayer();
this.periodicallyRefreshPlayerElement = false;
try {
@ -271,6 +272,7 @@ class PlayerData {
return;
}
if (
this.isFullscreen
|| (
@ -279,13 +281,15 @@ class PlayerData {
&& playerDimensions.height > 720
)
) {
this.ui = new UI(
'ultrawidifyUi',
{
parentElement: this.element,
eventBus: this.eventBus,
playerData: this,
uiSettings: this.videoData.settings.active.ui
uiSettings: this.videoData.settings.active.ui,
siteSettings: this.siteSettings,
}
);
@ -367,6 +371,15 @@ class PlayerData {
*
*/
trackDimensionChanges() {
if (this._isTrackDimensionChangesActive) {
// this shouldn't really get called, _ever_ ... but sometimes it happens
console.warn('[PlayerData::trackDimensionChanges] trackDimensionChanges is already active!');
return;
}
this._isTrackDimensionChangesActive = true;
try {
// get player dimensions _once_
let currentPlayerDimensions;
this.isFullscreen = !!document.fullscreenElement;
@ -400,6 +413,7 @@ class PlayerData {
this.eventBus.send('restore-ar', null);
this.eventBus.send('delayed-restore-ar', {delay: 500});
this.dimensions = currentPlayerDimensions;
this._isTrackDimensionChangesActive = false;
return;
}
@ -410,6 +424,11 @@ class PlayerData {
// Save current dimensions to avoid triggering this function pointlessly
this.dimensions = currentPlayerDimensions;
} catch (e) {
}
this._isTrackDimensionChangesActive = false;
}
@ -435,13 +454,13 @@ class PlayerData {
private handleDimensionChanges(newDimensions: PlayerDimensions, oldDimensions: PlayerDimensions) {
if (this.runLevel === RunLevel.Off ) {
this.logger.log('info', 'debug', "[PlayerDetect] player size changed, but PlayerDetect is in disabled state. The player element is probably too small.");
this.logger.info('handleDimensionChanges', "player size changed, but PlayerDetect is in disabled state. The player element is probably too small.");
return;
}
// 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)
this.logger.log('info', 'debug', "[PlayerDetect] player size potentially changed.\n\nold dimensions:", oldDimensions, '\nnew dimensions:', newDimensions);
this.logger.info('handleDimensionChanges', "player size potentially changed.\n\nold dimensions:", oldDimensions, '\nnew dimensions:', newDimensions);
// if size doesn't match, trigger onPlayerDimensionChange
if (
@ -463,10 +482,17 @@ class PlayerData {
}
}
onPlayerDimensionsChanged(mutationList?, observer?) {
onPlayerDimensionsChanged: ResizeObserverCallback = _.debounce(
(mutationList?, observer?) => {
this.trackDimensionChanges();
this.trackEnvironmentChanges();
},
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
}
);
//#region player element change detection
@ -480,29 +506,19 @@ class PlayerData {
}
try {
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
if (this.observer) {
this.observer.disconnect();
}
)
);
const observerConf = {
attributes: true,
// attributeFilter: ['style', 'class'],
attributeOldValue: true,
};
this.observer = new ResizeObserver(
this.onPlayerDimensionsChanged
);
this.observer.observe(this.element);
} catch (e) {
console.error("failed to set observer",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();
@ -574,12 +590,39 @@ class PlayerData {
return this.elementStack;
}
updatePlayer(options?: {verbose?: boolean, newElement?: HTMLElement}) {
const newPlayer = options?.newElement ?? this.getPlayer(options);
if (newPlayer === this.element) {
return;
}
// clean up and re-initialize UI
this.ui?.destroy();
delete this.ui;
this.element = newPlayer;
this.ui = new UI(
'ultrawidifyUi',
{
parentElement: this.element,
eventBus: this.eventBus,
playerData: this,
uiSettings: this.videoData.settings.active.ui,
siteSettings: this.siteSettings,
}
);
this.trackDimensionChanges();
this.trackEnvironmentChanges();
}
/**
* Finds and returns HTML element of the player
*/
getPlayer(options?: {verbose?: boolean}): HTMLElement {
const host = window.location.hostname;
let element = this.videoElement.parentNode;
private getPlayer(options?: {verbose?: boolean}): HTMLElement {
const videoWidth = this.videoElement.offsetWidth;
const videoHeight = this.videoElement.offsetHeight;
let playerCandidate;
@ -600,7 +643,6 @@ class PlayerData {
}
// if mode is given, we follow the preference
if (this.siteSettings.data.currentDOMConfig?.elements?.player?.manual && this.siteSettings.data.currentDOMConfig?.elements?.player?.mode) {
if (this.siteSettings.data.currentDOMConfig?.elements?.player?.mode === 'qs') {
playerCandidate = this.getPlayerQs(playerQs, elementStack, videoWidth, videoHeight);
@ -647,17 +689,12 @@ class PlayerData {
this.equalish(elementStack[currentIndex].element.offsetWidth, elementStack[nextIndex].element.offsetWidth, 2)
&& this.equalish(elementStack[currentIndex].element.offsetHeight, elementStack[nextIndex].element.offsetHeight, 2)
) {
this.siteSettings.set('playerAutoConfig.initialIndex', this.siteSettings.data.playerAutoConfig.initialIndex + 1, {noSave: true});
this.siteSettings.set('playerAutoConfig.modified', true);
console.log('updated site settings:', this.siteSettings.data.playerAutoConfig);
this.videoData.settings.saveWithoutReload();
// this.siteSettings.set('playerAutoConfig.initialIndex', this.siteSettings.data.playerAutoConfig.initialIndex + 1, {noSave: true});
// this.siteSettings.set('playerAutoConfig.modified', true);
// console.log('updated site settings:', this.siteSettings.data.playerAutoConfig);
// this.videoData.settings.saveWithoutReload();
this.ui?.destroy();
this.ui = undefined;
this.element = elementStack[nextIndex].element;
this.trackDimensionChanges();
this.trackEnvironmentChanges();
this.updatePlayer({newElement: elementStack[nextIndex].element});
}
}
@ -672,6 +709,7 @@ class PlayerData {
let penaltyMultiplier = 1;
const sizePenaltyMultiplier = 0.1;
const perLevelScorePenalty = 10;
let sameSizeBonus = 0;
for (const [index, element] of elementStack.entries()) {
element.index = index;
@ -720,7 +758,13 @@ class PlayerData {
// we prefer elements closer to the video, so the score of each potential
// candidate gets dinked a bit
score -= perLevelScorePenalty * penaltyMultiplier;
// score -= perLevelScorePenalty * penaltyMultiplier;
if (element.width === elementStack[index - 1].width && element.height === elementStack[index - 1].height) {
score += ++sameSizeBonus;
} else {
sameSizeBonus = 0;
}
element.autoScore = score;
element.heuristics['autoScoreDetails'] = {
@ -735,19 +779,37 @@ class PlayerData {
}
}
let bestCandidate: any = {autoScore: -99999999, initialValue: true};
for (const element of elementStack) {
if (element.autoScore > bestCandidate.autoScore) {
bestCandidate = element;
}
}
if (bestCandidate.initialValue) {
bestCandidate = null;
} else {
bestCandidate.heuristics['autoMatch'] = true;
if (this.siteSettings.data.playerAutoConfig?.initialIndex !== bestCandidate.index) {
this.siteSettings.set('playerAutoConfig.initialIndex', bestCandidate.index, {reload: false, scripted: true});
// if (this.siteSettings.data.playerAutoConfig?.initialIndex !== bestCandidate.index) {
// this.siteSettings.set('playerAutoConfig.initialIndex', bestCandidate.index, {reload: false, scripted: true});
// }
}
// BUT WAIT! THERE'S MORE
// Some sites (youtube) can re-parent elements, causing current player element to vanish from DOM
if (bestCandidate) {
const observer = new MutationObserver( (mutations) => {
mutations.forEach((mutation) => {
mutation.removedNodes.forEach((node) => {
if (node === bestCandidate.element) {
observer.disconnect();
this.updatePlayer();
}
})
});
});
observer.observe(bestCandidate.element.parentNode, {childList: true});
}
return bestCandidate;
@ -817,7 +879,7 @@ class PlayerData {
*/
private handlePlayerTreeRequest() {
// this populates this.elementStack fully
this.getPlayer({verbose: true});
this.updatePlayer({verbose: true});
console.log('tree:', JSON.parse(JSON.stringify(this.elementStack)));
console.log('————————————————————— handling player tree request!')
this.eventBus.send('uw-config-broadcast', {type: 'player-tree', config: JSON.parse(JSON.stringify(this.elementStack))});
@ -858,13 +920,7 @@ class PlayerData {
}
forceRefreshPlayerElement() {
this.ui?.destroy();
this.ui = undefined;
this.element = this.getPlayer();
// this.notificationService?.replace(this.element);
this.trackDimensionChanges();
this.trackEnvironmentChanges();
this.updatePlayer();
}
}

View File

@ -1,11 +1,7 @@
import Debug from '../../conf/Debug';
import PlayerData from './PlayerData';
import Resizer from '../video-transform/Resizer';
import AspectRatioType from '../../../common/enums/AspectRatioType.enum';
import CropModePersistence from '../../../common/enums/CropModePersistence.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';
@ -19,6 +15,8 @@ import { Aard } from '../aard/Aard';
import { Stretch } from '../../../common/interfaces/StretchInterface';
import ExtensionMode from '../../../common/enums/ExtensionMode.enum';
import { ExtensionEnvironment } from '../../../common/interfaces/SettingsInterface';
import { LogAggregator } from '../logging/LogAggregator';
import { ComponentLogger } from '../logging/ComponentLogger';
/**
* VideoData handles CSS for the video element.
@ -67,7 +65,8 @@ class VideoData {
//#endregion
//#region helper objects
logger: Logger;
logger: ComponentLogger;
logAggregator: LogAggregator
settings: Settings; // AARD needs it
siteSettings: SiteSettings;
pageInfo: PageInfo;
@ -117,7 +116,9 @@ class VideoData {
* @param pageInfo
*/
constructor(video, settings: Settings, siteSettings: SiteSettings, pageInfo: PageInfo){
this.logger = pageInfo.logger;
this.logAggregator = pageInfo.logAggregator;
this.logger = new ComponentLogger(this.logAggregator, 'VideoData', {});
this.arSetupComplete = false;
this.video = video;
this.destroyed = false;
@ -167,7 +168,7 @@ class VideoData {
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.logger.info('onVideoLoaded', '%c ——————————— Initiating phase two of videoData setup ———————————', 'color: #0f9');
this.hasDrm = hasDrm(this.video);
this.eventBus.send(
@ -179,12 +180,12 @@ class VideoData {
this.videoDimensionsLoaded = true;
try {
await this.setupStageTwo();
this.logger.log('info', 'init', '%c[VideoData::onVideoLoaded] ——————————— videoData setup stage two complete ———————————', 'color: #0f9');
this.logger.info('onVideoLoaded', '%c——————————— 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);
this.logger.error('onVideoLoaded', '%c ——————————— 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.logger.debug('onVideoLoaded', "%cRecovering from illegal video dimensions. Resetting aspect ratio.", "background: #afd, color: #132");
this.restoreCrop();
this.videoDimensionsLoaded = true;
@ -213,11 +214,11 @@ class VideoData {
//#region <video> event handlers
onLoadedData() {
this.logger.log('info', 'init', '[VideoData::ctor->video.onloadeddata] Video fired event "loaded data!"');
this.logger.info('onLoadedData', 'Video fired event "loaded data!"');
this.onVideoLoaded();
}
onLoadedMetadata() {
this.logger.log('info', 'init', '[VideoData::ctor->video.onloadedmetadata] Video fired event "loaded metadata!"');
this.logger.log('onLoadedData', 'Video fired event "loaded metadata!"');
this.onVideoLoaded();
}
onTimeUpdate() {
@ -231,7 +232,7 @@ class VideoData {
* Sets up event listeners for this video
*/
async setupEventListeners() {
this.logger.log('info', 'init', '%c[VideoData::setupEventListeners] ——————————— Starting event listener setup! ———————————', 'color: #0f9');
this.logger.info('setupEventListeners', '%c——————————— Starting event listener setup! ———————————', 'color: #0f9');
// this is in case extension loads before the video
this.video.addEventListener('loadeddata', this.onLoadedData.bind(this));
@ -240,7 +241,7 @@ class VideoData {
// 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::setupEventListeners] ——————————— Event listeners setup complete! ———————————', 'color: #0f9');
this.logger.info('setupEventListeners', '%c——————————— Event listeners setup complete! ———————————', 'color: #0f9');
}
/**
@ -264,7 +265,7 @@ class VideoData {
}
this.logger.log('info', ['debug', 'init'], '[VideoData::ctor] Created videoData with vdid', this.vdid);
this.logger.info('setupStageTwo', 'Created videoData with vdid', this.vdid);
// Everything is set up at this point. However, we are still purely "read-only" at this point. Player CSS should not be changed until
@ -363,7 +364,7 @@ class VideoData {
* 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`);
this.logger.info('destroy', `<vdid:${this.vdid}> received destroy command`);
if (this.video) {
this.video.classList.remove(this.userCssClassName);
@ -403,7 +404,8 @@ class VideoData {
return;
}
if (this.currentEnvironment !== this.player.environment) {
console.warn('environment changed to:', this.player.environment);
this.logger.warn('onEnvironmentChanged', 'environment changed from:', this.currentEnvironment, 'to:', this.player.environment);
this.currentEnvironment = this.player.environment;
if (this.siteSettings.data.enable[this.player.environment] === ExtensionMode.Disabled) {
this.setRunLevel(RunLevel.Off);
@ -473,10 +475,10 @@ class VideoData {
restoreCrop() {
if (!this.resizer) {
this.logger.log('warn', 'debug', '[VideoData::restoreCrop] Resizer has not been initialized yet. Crop will not be restored.');
this.logger.warn('restoreCrop', 'Resizer has not been initialized yet. Crop will not be restored.');
return;
}
this.logger.log('info', 'debug', '[VideoData::restoreCrop] Attempting to reset aspect ratio.')
this.logger.info('restoreCrop', 'Attempting to reset aspect ratio.');
// if we have default crop set for this page, apply this.
// otherwise, reset crop
@ -489,7 +491,7 @@ class VideoData {
this.stopArDetection();
this.startArDetection();
} catch (e) {
this.logger.log('warn', 'debug', '[VideoData::restoreCrop] Autodetection not resumed. Reason:', e);
this.logger.warn('restoreCrop', 'Autodetection not resumed. Reason:', e);
}
}
}
@ -516,13 +518,13 @@ class VideoData {
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.logger.error('onVideoMutation', 'mutation was triggered, but video element is missing. Something is fishy. Terminating this uw instance.');
this.destroy();
return;
}
if (!this.enabled) {
this.logger.log('info', 'info', '[VideoData::onVideoMutation] mutation was triggered, but the extension is disabled. Is the player window too small?');
this.logger.info('onVideoMutation', 'mutation was triggered, but the extension is disabled. Is the player window too small?');
return;
}
@ -552,8 +554,8 @@ class VideoData {
onVideoDimensionsChanged(mutationList, observer) {
if (!mutationList || this.video === undefined) { // something's wrong
if (observer && this.video) {
this.logger.log(
'warn', 'debug',
this.logger.warn(
'onVideoDimensionChanged',
'onVideoDimensionChanged encountered a weird state. video and observer exist, but mutationlist does not.\n\nmutationList:', mutationList,
'\nobserver:', observer,
'\nvideo:', this.video,
@ -573,7 +575,7 @@ class VideoData {
*/
private _processDimensionsChanged() {
if (!this.player) {
this.logger.log('warn', 'debug', `[VideoData::_processDimensionsChanged] Player is not defined. This is super haram.`, this.player);
this.logger.warn('_processDimensionsChanged', `Player is not defined. This is super haram.`, this.player);
return;
}
// adding player observer taught us that if element size gets triggered by a class, then
@ -706,7 +708,7 @@ class VideoData {
startArDetection() {
this.logger.log('info', 'debug', "[VideoData::startArDetection] starting AR detection")
this.logger.info('startArDetection', 'starting AR detection');
if(this.destroyed || this.invalid) {
// throw {error: 'VIDEO_DATA_DESTROYED', data: {videoData: this}};
return;
@ -725,7 +727,7 @@ class VideoData {
}
this.aard.startCheck();
} catch (e) {
this.logger.log('warn', 'debug', '[VideoData::startArDetection()] Could not start aard for some reason. Was the function was called too early?', e);
this.logger.warn('startArDetection', 'Could not start aard for some reason. Was the function was called too early?', e);
}
}
@ -754,17 +756,16 @@ class VideoData {
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");
this.logger.warn('checkVideoSizeChange', "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);
this.logger.debug('checkVideoSizeChange', "player size changed. reason: dimension change. Old dimensions?", this.dimensions.width, this.dimensions.height, "new dimensions:", this.video.offsetWidth, this.video.offsetHeight);
}
}

View File

@ -3,29 +3,29 @@ 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 siteSettings from '../Settings';
import VideoData from '../video-data/VideoData';
import EventBus from '../EventBus';
import { _cp } from '../../../common/js/utils';
import Settings from '../Settings';
import { Ar } from '../../../common/interfaces/ArInterface';
import { Ar, ArVariant } from '../../../common/interfaces/ArInterface';
import { RunLevel } from '../../enum/run-level.enum';
import * as _ from 'lodash';
import getElementStyles from '../../util/getElementStyles';
import { Stretch } from '../../../common/interfaces/StretchInterface';
import { ComponentLogger } from '../logging/ComponentLogger';
if(Debug.debug) {
console.log("Loading: Resizer.js");
}
enum ResizerMode {
Crop = 1,
Zoom = 2
};
/**
* Resizer is the top class and is responsible for figuring out which component needs to crop, which
* component needs to zoom, and which component needs to stretch.
@ -41,7 +41,7 @@ class Resizer {
//#endregion
//#region helper objects
logger: Logger;
logger: ComponentLogger;
settings: Settings;
siteSettings: SiteSettings;
scaler: Scaler;
@ -63,6 +63,8 @@ class Resizer {
currentCssValidFor: any;
currentVideoSettings: any;
private effectiveZoom: {x: number, y: number} = {x: 1, y: 1};
_lastAr: Ar = {type: AspectRatioType.Initial};
set lastAr(x: Ar) {
// emit updates for UI when setting lastAr, but only if AR really changed
@ -83,17 +85,22 @@ class Resizer {
//#endregion
cycleableAspectRatios: Ar[];
cycleableZoomAspectRatios: Ar[];
nextCycleOptionIndex = 0;
//#region event bus configuration
private eventBusCommands = {
'get-effective-zoom': [{
function: () => {
this.eventBus.send('announce-zoom', this.manualZoom ? {x: this.zoom.scale, y: this.zoom.scaleY} : this.zoom.effectiveZoom);
}
}],
'set-ar': [{
function: (config: any) => {
this.manualZoom = false; // this only gets called from UI or keyboard shortcuts, making this action safe.
if (config.type !== AspectRatioType.Cycle) {
this.setAr(config);
this.setAr({...config, variant: ArVariant.Crop});
} else {
// if we manually switched to a different aspect ratio, cycle from that ratio forward
const lastArIndex = this.cycleableAspectRatios.findIndex(x => x.type === this.lastAr.type && x.ratio === this.lastAr.ratio);
@ -101,11 +108,29 @@ class Resizer {
this.nextCycleOptionIndex = (lastArIndex + 1) % this.cycleableAspectRatios.length;
}
this.setAr(this.cycleableAspectRatios[this.nextCycleOptionIndex]);
this.setAr({...this.cycleableAspectRatios[this.nextCycleOptionIndex], variant: ArVariant.Crop});
this.nextCycleOptionIndex = (this.nextCycleOptionIndex + 1) % this.cycleableAspectRatios.length;
}
}
}],
'set-ar-zoom': [{
function: (config: any) => {
this.manualZoom = false; // this only gets called from UI or keyboard shortcuts, making this action safe.
if (config.type !== AspectRatioType.Cycle) {
this.setAr({...config, variant: ArVariant.Zoom});
} else {
// if we manually switched to a different aspect ratio, cycle from that ratio forward
const lastArIndex = this.cycleableZoomAspectRatios.findIndex(x => x.type === this.lastAr.type && x.ratio === this.lastAr.ratio);
if (lastArIndex !== -1) {
this.nextCycleOptionIndex = (lastArIndex + 1) % this.cycleableZoomAspectRatios.length;
}
this.setAr({...this.cycleableZoomAspectRatios[this.nextCycleOptionIndex], variant: ArVariant.Zoom});
this.nextCycleOptionIndex = (this.nextCycleOptionIndex + 1) % this.cycleableZoomAspectRatios.length;
}
}
}],
'set-alignment': [{
function: (config: any) => {
this.setVideoAlignment(config.x, config.y);
@ -118,10 +143,12 @@ class Resizer {
}
}],
'set-zoom': [{
function: (config: any) => this.setZoom(config.zoom, config.axis, config.noAnnounce)
function: (config: any) => {
this.setZoom(config?.zoom ?? {zoom: 1});
}
}],
'change-zoom': [{
function: (config: any) => this.zoomStep(config.step)
function: (config: any) => this.zoomStep(config.zoom)
}],
'get-ar': [{
function: () => this.eventBus.send('uw-config-broadcast', {type: 'ar', config: this.lastAr})
@ -157,7 +184,7 @@ class Resizer {
constructor(videoData) {
this.resizerId = (Math.random()*100).toFixed(0);
this.videoData = videoData;
this.logger = videoData.logger;
this.logger = new ComponentLogger(videoData.logAggregator, 'Resizer');
this.video = videoData.video;
this.settings = videoData.settings;
this.siteSettings = videoData.siteSettings;
@ -188,6 +215,11 @@ class Resizer {
.filter(x => [AspectRatioType.FitHeight, AspectRatioType.FitWidth, AspectRatioType.Fixed, AspectRatioType.Reset].includes(x?.arguments?.type))
.map(x => x.arguments) as Ar[];
this.cycleableZoomAspectRatios =
(this.settings?.active?.commands?.zoom ?? [])
.filter(x => x.action === 'set-ar-zoom' && x.arguments?.type !== AspectRatioType.Cycle)
.map(x => x.arguments) as Ar[];
this.nextCycleOptionIndex = 0;
this.userCssClassName = videoData.userCssClassName;
}
@ -206,7 +238,7 @@ class Resizer {
}
destroy(){
this.logger.log('info', ['debug', 'init'], `[Resizer::destroy] <rid:${this.resizerId}> received destroy command.`);
this.logger.info('destroy', `<rid:${this.resizerId}> received destroy command.`);
this.destroyed = true;
}
@ -223,7 +255,7 @@ class Resizer {
let ratioOut;
if (!this.videoData.video) {
this.logger.log('info', 'debug', "[Scaler.js::modeToAr] No video??",this.videoData.video, "killing videoData");
this.logger.info('calculateRatioForLegacyOptions', "No video??", this.videoData.video, "killing videoData");
this.videoData.destroy();
return null;
}
@ -232,7 +264,7 @@ class Resizer {
if (! this.videoData.player.dimensions) {
ratioOut = screen.width / screen.height;
} else {
this.logger.log('info', 'debug', `[Resizer::calculateRatioForLegacyOptions] <rid:${this.resizerId}> Player dimensions:`, this.videoData.player.dimensions.width ,'x', this.videoData.player.dimensions.height,'aspect ratio:', this.videoData.player.dimensions.width / this.videoData.player.dimensions.height)
this.logger.info('calculateRatioForLegacyOptions', `<rid:${this.resizerId}> Player dimensions:`, this.videoData.player.dimensions.width ,'x', this.videoData.player.dimensions.height,'aspect ratio:', this.videoData.player.dimensions.width / this.videoData.player.dimensions.height)
ratioOut = this.videoData.player.dimensions.width / this.videoData.player.dimensions.height;
}
@ -248,7 +280,7 @@ class Resizer {
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);
this.logger.info('modeToAr', "Using original aspect ratio -", fileAr);
ar.ratio = fileAr;
} else {
return null;
@ -257,7 +289,7 @@ class Resizer {
return ar;
}
updateAr(ar) {
updateAr(ar: Ar) {
if (!ar) {
return;
}
@ -276,8 +308,27 @@ class Resizer {
}
}
/**
* Starts and stops Aard as necessary. Returns 'true' if we can
* stop setting aspect ratio early.
* @param ar
* @param resizerMode
* @returns
*/
private handleAard(ar: Ar): boolean {
if (ar.type === AspectRatioType.Automatic) {
this.videoData.aard?.startCheck(ar.variant);
return true;
} else if (ar.type !== AspectRatioType.AutomaticUpdate) {
this.videoData.aard?.stop();
} else if (this.stretcher.stretch.type === StretchType.Basic) {
this.videoData?.aard?.stop();
}
}
async setAr(ar: Ar, lastAr?: Ar) {
if (this.destroyed) {
if (this.destroyed || ar == null) {
return;
}
@ -294,11 +345,8 @@ class Resizer {
}
// handle autodetection stuff
if (ar.type === AspectRatioType.Automatic) {
this.videoData.aard?.startCheck();
if (this.handleAard(ar)) {
return;
} else if (ar.type !== AspectRatioType.AutomaticUpdate) {
this.videoData.aard?.stop();
}
if (ar.type !== AspectRatioType.AutomaticUpdate) {
@ -306,15 +354,11 @@ class Resizer {
}
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.logger.warn('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.videoData.videoUnloaded();
}
this.logger.log('info', 'debug', '%c[Resizer::setAr] <rid:'+this.resizerId+'> trying to set ar. New ar:', 'background-color: #4c3a2f, color: #ffa349', ar);
if (ar == null) {
return;
}
this.logger.info('setAr', `<rid:${this.resizerId}> trying to set ar. New ar:`, ar);
let stretchFactors: VideoDimensions | any;
@ -324,6 +368,7 @@ class Resizer {
(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
|| ar.variant !== this.lastAr.variant
) {
this.zoom.reset();
this.resetPan();
@ -350,7 +395,7 @@ class Resizer {
// 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.`);
this.logger.info('setAr', `<rid:${this.resizerId}> Something wrong with ar or the player. Doing nothing.`);
return;
}
this.lastAr = {type: ar.type, ratio: ar.ratio};
@ -360,27 +405,12 @@ class Resizer {
this.videoData.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 && ar.type !== AspectRatioType.AutomaticUpdate) || this.stretcher.stretch.type === StretchType.Basic) {
this.videoData?.aard?.stop();
} else {
if (ar.type !== AspectRatioType.AutomaticUpdate) {
if (this.lastAr.type === AspectRatioType.Automatic || this.lastAr.type === AspectRatioType.AutomaticUpdate) {
this.videoData?.aard?.stop();
}
}
}
// do stretch thingy
if ([StretchType.NoStretch, StretchType.Conditional, StretchType.FixedSource].includes(this.stretcher.stretch.type)) {
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 || stretchFactors.error){
this.logger.error('setAr', ` <rid:${this.resizerId}> failed to set AR due to problem with calculating crop. Error:`, stretchFactors?.error);
if (stretchFactors?.error === 'no_video'){
this.videoData.destroy();
return;
@ -401,25 +431,24 @@ class Resizer {
} else if (this.stretcher.stretch.type === StretchType.FixedSource) {
this.stretcher.applyStretchFixedSource(stretchFactors);
}
this.logger.log('info', 'debug', "[Resizer::setAr] Processed stretch factors for ",
this.logger.info('setAr', "Processed stretch factors for ",
this.stretcher.stretch.type === StretchType.NoStretch ? 'stretch-free crop.' :
this.stretcher.stretch.type === StretchType.Conditional ? 'crop with conditional StretchType.' : 'crop with fixed stretch',
'Stretch factors are:', stretchFactors
);
} else if (this.stretcher.stretch.type === 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);
this.logger.info('setAr', 'Processed stretch factors for hybrid stretch/crop. Stretch factors are:', stretchFactors);
} else if (this.stretcher.stretch.type === StretchType.Fixed) {
stretchFactors = this.stretcher.calculateStretchFixed(ar.ratio)
stretchFactors = this.stretcher.calculateStretchFixed(ar.ratio);
} else if (this.stretcher.stretch.type === StretchType.Basic) {
stretchFactors = this.stretcher.calculateBasicStretch();
this.logger.log('info', 'debug', '[Resizer::setAr] Processed stretch factors for basic StretchType. Stretch factors are:', stretchFactors);
this.logger.log('setAr', 'Processed stretch factors for basic StretchType. Stretch factors are:', stretchFactors);
} else {
stretchFactors = this.scaler.calculateCrop(ar);
this.logger.log(
'error', 'debug',
'[Resizer::setAr] Okay wtf happened? If you see this, something has gone wrong. Pretending stretchMode is set tu NoStretch. Stretch factors are:', stretchFactors,
this.logger.error(
'setAr',
'Okay wtf happened? If you see this, something has gone wrong. Pretending stretchMode is set tu NoStretch. Stretch factors are:', stretchFactors,
"\n------[ i n f o d u m p ]------\nstretcher:", this.stretcher,
'\nargs: ar (corrected for legacy):', ar, 'last ar (optional argument):', lastAr
);
@ -429,12 +458,12 @@ class Resizer {
}
applyScaling(stretchFactors: VideoDimensions, options?: {noAnnounce?: boolean, ar?: Ar}) {
// this.stretcher.chromeBugMitigation(stretchFactors);
this.zoom.effectiveZoom = {x: stretchFactors.xFactor, y: stretchFactors.yFactor};
// let the UI know
if(!options?.noAnnounce) {
this.videoData.eventBus.send('announce-zoom', {x: stretchFactors.xFactor, y: stretchFactors.yFactor});
}
// announcing zoom somehow keeps incorrectly resetting zoom sliders in UI — UI is now polling for effective zoom while visible
// if(!options?.noAnnounce) {
// this.videoData.eventBus.send('announce-zoom', this.manualZoom ? {x: this.zoom.scale, y: this.zoom.scaleY} : this.zoom.effectiveZoom);
// }
let translate = this.computeOffsets(stretchFactors, options?.ar);
this.applyCss(stretchFactors, translate);
@ -476,7 +505,7 @@ class Resizer {
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.logger.info({src: 'panHandler', origin: 'mousemove'}, "mousemove.pageX, pageY:", event.pageX, event.pageY, "\nrelativeX/Y:", relativeX, relativeY);
this.setPan(relativeX, relativeY);
}
@ -539,7 +568,7 @@ class Resizer {
*/
restore() {
if (!this.manualZoom) {
this.logger.log('info', 'debug', "[Resizer::restore] <rid:"+this.resizerId+"> attempting to restore aspect ratio", {'lastAr': this.lastAr} );
this.logger.info('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){
@ -577,9 +606,36 @@ class Resizer {
}
}
setZoom(zoomLevel: number, axis?: 'x' | 'y', noAnnounce?) {
private _setZoomTimeout;
private _latestSetZoomArgs: any | undefined;
private _SET_ZOOM_RATE_LIMIT_MS = 50;
/**
* Sets zoom level. This function is rate limited, because slider may spam the fuck out of this function call
* @param zoomLevel
* @param axis
* @param noAnnounce
* @returns
*/
setZoom(zoomLevel: number | {x: number, y: number}, noAnnounce?) {
if (this._setZoomTimeout) {
this._latestSetZoomArgs = {zoomLevel, noAnnounce};
return;
}
this.manualZoom = true;
this.zoom.setZoom(zoomLevel, axis, noAnnounce);
this.zoom.setZoom(zoomLevel);
this._setZoomTimeout = setTimeout(
() => {
clearTimeout(this._setZoomTimeout);
this._setZoomTimeout = undefined;
if (this._latestSetZoomArgs) {
this.setZoom(this._latestSetZoomArgs.zoomLevel);
}
this._latestSetZoomArgs = undefined;
},
this._SET_ZOOM_RATE_LIMIT_MS
);
}
zoomStep(step){
@ -697,7 +753,7 @@ class Resizer {
private _computeOffsetsRecursionGuard: boolean = false;
computeOffsets(stretchFactors: VideoDimensions, ar?: Ar){
this.logger.log('info', 'debug', "[Resizer::computeOffsets] <rid:"+this.resizerId+"> video will be aligned to ", this.videoAlignment);
this.logger.info('computeOffsets', `<rid:${this.resizerId}> video will be aligned to `, this.videoAlignment, '— stretch factors before processing:', stretchFactors);
const {realVideoWidth, realVideoHeight, marginX, marginY} = this.computeVideoDisplayedDimensions();
@ -765,8 +821,9 @@ class Resizer {
}
}
this.logger.log(
'info', ['debug', 'resizer'], "[Resizer::_res_computeOffsets] <rid:"+this.resizerId+"> calculated offsets:",
this.logger.info(
'computeOffsets',
`<rid:${this.resizerId}> calculated offsets:`,
'\n\n---- elements ----',
'\nplayer element: ', this.videoData.player.element,
'\nvideo element: ', this.videoData.video,
@ -794,13 +851,15 @@ class Resizer {
// 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.videoData.video.offsetWidth > this.videoData.player.dimensions.width && this.videoData.video.offsetHeight > this.videoData.player.dimensions.height) ||
(this.videoData.video.offsetWidth < this.videoData.player.dimensions.width && this.videoData.video.offsetHeight < this.videoData.player.dimensions.height)
) && ar?.variant !== ArVariant.Zoom
) {
this.logger.log('warn', ['debugger', 'resizer'], `[Resizer::_res_computeOffsets] <rid:${this.resizerId}> We are getting some incredibly funny results here.\n\n`,
this.logger.warn('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.
@ -874,13 +933,13 @@ class Resizer {
// 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.logger.warn('applyCss', `<rid:${this.resizerId}> Video went missing, doing nothing.`);
this.videoData.destroy();
return;
}
this.logger.log('info', ['debug', 'resizer'], "[Resizer::applyCss] <rid:"+this.resizerId+"> will apply css.", {stretchFactors, translate});
this.logger.info('applyCss', `<rid:${this.resizerId}> will apply css.`, {stretchFactors, translate});
// save stuff for quick tests (before we turn numbers into css values):
this.currentVideoSettings = {
@ -915,18 +974,18 @@ class Resizer {
// 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.logger.debug('setStyleString', `<rid:${this.resizerId}> Setting new css: `, newCssString);
this.eventBus.send('inject-css', {cssString: 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);
this.logger.debug('setStyleString', `<rid:${this.resizerId}}> Replacing css.\nOld string:`, this.userCss, "\nNew string:", newCssString)
// we only replace css if it
this.eventBus.send('replace-css', {oldCssString: 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.");
this.logger.debug('setStyleString', `<rid:${this.resizerId}> Existing css is still valid, doing nothing.`);
}
}

View File

@ -1,8 +1,7 @@
import Debug from '../../conf/Debug';
import AspectRatioType from '../../../common/enums/AspectRatioType.enum';
import BrowserDetect from '../../conf/BrowserDetect';
import VideoData from '../video-data/VideoData';
import Logger from '../Logger';
import { Ar, ArVariant } from '../../../common/interfaces/ArInterface';
import { ComponentLogger } from '../logging/ComponentLogger';
export enum CropStrategy {
@ -45,13 +44,13 @@ export type VideoDimensions = {
class Scaler {
//#region helper objects
conf: VideoData;
logger: Logger;
logger: ComponentLogger;
//#endregion
// functions
constructor(videoData) {
this.conf = videoData;
this.logger = videoData.logger;
this.logger = new ComponentLogger(videoData.logAggregator, 'Scaler', {});
}
@ -64,7 +63,7 @@ class Scaler {
let ratioOut;
if (!this.conf.video) {
this.logger.log('error', 'debug', "[Scaler.js::modeToAr] No video??",this.conf.video, "killing videoData");
this.logger.error('modeToAr', "No video??",this.conf.video, "killing videoData");
this.conf.destroy();
return null;
}
@ -92,7 +91,7 @@ class Scaler {
return ratioOut;
}
else if (ar.type === AspectRatioType.Reset) {
this.logger.log('info', 'debug', "[Scaler.js::modeToAr] Using original aspect ratio -", fileAr)
this.logger.info('modeToAr', "Using original aspect ratio -", fileAr)
ar.ar = fileAr;
return fileAr;
}
@ -100,7 +99,7 @@ class Scaler {
return null;
}
calculateCrop(ar: {type: AspectRatioType, ratio?: number}): VideoDimensions | {error: string, [x: string]: any} {
calculateCrop(ar: Ar): VideoDimensions | {error: string, [x: string]: any} {
/**
* STEP 1: NORMALIZE ASPECT RATIO
*
@ -136,7 +135,7 @@ class Scaler {
}
if(!this.conf.video){
this.logger.log('info', 'debug', "[Scaler::calculateCrop] ERROR — no video detected. Conf:", this.conf, "video:", this.conf.video, "video dimensions:", this.conf.video && this.conf.video.videoWidth, '×', this.conf.video && this.conf.video.videoHeight);
this.logger.info('calculateCrop', "ERROR — no video detected. Conf:", this.conf, "video:", this.conf.video, "video dimensions:", this.conf.video && this.conf.video.videoWidth, '×', this.conf.video && this.conf.video.videoHeight);
this.conf.destroy();
return {error: "no_video"};
@ -144,7 +143,7 @@ class Scaler {
if (this.conf.video.videoWidth == 0 || this.conf.video.videoHeight == 0) {
// that's illegal, but not illegal enough to just blast our shit to high hell
// mr officer will let you go with a warning this time around
this.logger.log('error', 'debug', "[Scaler::calculateCrop] Video has illegal dimensions. Video dimensions:", this.conf.video && this.conf.video.videoWidth, '×', this.conf.video && this.conf.video.videoHeight);
this.logger.error('calculateCrop', "Video has illegal dimensions. Video dimensions:", this.conf.video && this.conf.video.videoWidth, '×', this.conf.video && this.conf.video.videoHeight);
return {error: "illegal_video_dimensions"};
}
@ -164,16 +163,22 @@ class Scaler {
// handle fuckie-wuckies
if (!ar.ratio){
this.logger.log('error', 'scaler', "[Scaler::calculateCrop] no ar?", ar.ratio, " -- we were given this mode:", ar);
this.logger.error('calculateCrop', "no ar?", ar.ratio, " -- we were given this mode:", ar);
return {error: "no_ar", ratio: ar.ratio};
}
this.logger.log('info', 'scaler', "[Scaler::calculateCrop] trying to set ar. args are: ar->",ar.ratio,"; this.conf.player.dimensions->",this.conf.player.dimensions.width, "×", this.conf.player.dimensions.height, "| obj:", this.conf.player.dimensions);
this.logger.info('calculateCrop', "trying to set ar. args are: ar->",ar.ratio,"; this.conf.player.dimensions->",this.conf.player.dimensions.width, "×", this.conf.player.dimensions.height, "| obj:", this.conf.player.dimensions);
// If we encounter invalid players, we try to update its dimensions
// ONCE before throwing an error
if( (! this.conf.player.dimensions) || this.conf.player.dimensions.width === 0 || this.conf.player.dimensions.height === 0 ){
this.logger.error('calculateCrop', "ERROR — no (or invalid) this.conf.player.dimensions:",this.conf.player.dimensions);
this.conf.player.updatePlayer();
if( (! this.conf.player.dimensions) || this.conf.player.dimensions.width === 0 || this.conf.player.dimensions.height === 0 ){
this.logger.log('error', 'scaler', "[Scaler::calculateCrop] ERROR — no (or invalid) this.conf.player.dimensions:",this.conf.player.dimensions);
return {error: "this.conf.player.dimensions_error"};
}
}
// we can finally start computing required video dimensions now:
@ -184,7 +189,7 @@ class Scaler {
}
this.logger.log('info', 'scaler', "[Scaler::calculateCrop] ar is " ,ar.ratio, ", file ar is", streamAr, ", this.conf.player.dimensions are ", this.conf.player.dimensions.width, "×", this.conf.player.dimensions.height, "| obj:", this.conf.player.dimensions);
this.logger.info('calculateCrop', "ar is " ,ar.ratio, ", file ar is", streamAr, ",ar variant", ar.variant ,"\nthis.conf.player.dimensions are ", this.conf.player.dimensions.width, "×", this.conf.player.dimensions.height, "| obj:", this.conf.player.dimensions, this.conf.player.element);
const videoDimensions: VideoDimensions = {
xFactor: 1,
@ -199,7 +204,7 @@ class Scaler {
}
}
this.calculateCropCore(videoDimensions, ar.ratio, streamAr, playerAr)
this.calculateCropCore(videoDimensions, ar.ratio, streamAr, playerAr, ar.variant)
return videoDimensions;
}
@ -212,7 +217,11 @@ class Scaler {
* @param {*} streamAr
* @param {*} playerAr
*/
calculateCropCore(videoDimensions: VideoDimensions, ar: number, streamAr: number, playerAr: number) {
calculateCropCore(videoDimensions: VideoDimensions, ar: number, streamAr: number, playerAr: number, variant?: ArVariant) {
if (variant === ArVariant.Zoom) {
playerAr = ar;
}
if (streamAr < playerAr) {
if (streamAr < ar){
// in this situation we have to crop letterbox on top/bottom of the player
@ -254,8 +263,8 @@ class Scaler {
const letterboxRatio = (1 - (playerAr / ar));
videoDimensions.relativeCropLimits = {
top: ar > streamAr ? ( ar > playerAr ? (letterboxRatio * -0.5) : 0) : 0,
left: ar < streamAr ? ( ar < playerAr ? (-0.5 / letterboxRatio) : 0) : 0,
top: ar > streamAr ? ( ar >= playerAr ? (letterboxRatio * -0.5) : 0) : 0,
left: ar < streamAr ? ( ar <= playerAr ? (-0.5 / letterboxRatio) : 0) : 0,
}
videoDimensions.preventAlignment = {
x: ar > playerAr, // video is wider than player, so it's full width already

View File

@ -1,11 +1,9 @@
import { SiteSettings } from './../settings/SiteSettings';
import StretchType from '../../../common/enums/StretchType.enum';
import BrowserDetect from '../../conf/BrowserDetect';
import AspectRatioType from '../../../common/enums/AspectRatioType.enum';
import VideoData from '../video-data/VideoData';
import Logger from '../Logger';
import Settings from '../Settings';
import { Stretch } from '../../../common/interfaces/StretchInterface';
import { ComponentLogger } from '../logging/ComponentLogger';
// računa vrednosti za transform-scale (x, y)
// transform: scale(x,y) se uporablja za raztegovanje videa, ne pa za približevanje
@ -19,7 +17,7 @@ class Stretcher {
//#region helper objects
conf: VideoData;
logger: Logger;
logger: ComponentLogger;
settings: Settings;
siteSettings: SiteSettings;
//#endregion
@ -29,7 +27,7 @@ class Stretcher {
// functions
constructor(videoData) {
this.conf = videoData;
this.logger = videoData.logger;
this.logger = new ComponentLogger(videoData.logAggregator, 'Stretcher', {});;
this.siteSettings = videoData.siteSettings;
this.settings = videoData.settings;
@ -111,7 +109,7 @@ class Stretcher {
// * we squeeze X axis, if target AR is narrower than player size
// * we squeeze Y axis, if target AR is wider than the player size
this.logger.log('info', 'stretcher', `[Stretcher::applyStretchFixedSource] here's what we got:
this.logger.info('applyStretchFixedSource', `here's what we got:
postCropStretchFactors: x=${postCropStretchFactors.xFactor} y=${postCropStretchFactors.yFactor}
fixedStretchRatio: ${this.stretch.ratio}
videoAr: ${streamAr}
@ -120,7 +118,7 @@ squeezeFactor: ${squeezeFactor}`, '\nvideo', this.conf.video);
postCropStretchFactors.xFactor *= squeezeFactor;
this.logger.log('info', 'stretcher', `[Stretcher::applyStretchFixedSource] here's what we'll apply:\npostCropStretchFactors: x=${postCropStretchFactors.x} y=${postCropStretchFactors.y}`);
this.logger.info('applyStretchFixedSource', `here's what we'll apply:\npostCropStretchFactors: x=${postCropStretchFactors.x} y=${postCropStretchFactors.y}`);
return postCropStretchFactors;
}

View File

@ -1,9 +1,12 @@
import Debug from '../../conf/Debug';
import Logger from '../Logger';
import { ComponentLogger } from '../logging/ComponentLogger';
import VideoData from '../video-data/VideoData';
// računa približevanje ter računa/popravlja odmike videa
// calculates zooming and video offsets/panning
const MIN_SCALE = 0.5;
const MAX_SCALE = 8;
const LOG_MAX_SCALE = Math.log2(MAX_SCALE);
const LOG_MIN_SCALE = Math.log2(MIN_SCALE);
class Zoom {
//#region flags
@ -12,7 +15,7 @@ class Zoom {
//#region helper objects
conf: VideoData;
logger: Logger;
logger: ComponentLogger;
//#endregion
//#region misc data
@ -21,86 +24,81 @@ class Zoom {
logScale: number = 0;
logScaleY: number = 0;
scaleStep: number = 0.1;
minScale: number = -1; // 50% (log2(0.5) = -1)
maxScale: number = 3; // 800% (log2(8) = 3)
logMinScale: number = -1; // 50% (log2(0.5) = -1)
logMaxScale: number = 3; // 800% (log2(8) = 3)
minScale = 0.5;
maxScale = 8;
//#endregion
effectiveZoom: {x: number, y: number}; // we're setting this in Resizer based on Resizer data!
constructor(videoData) {
this.conf = videoData;
this.logger = videoData.logger;
this.logger = new ComponentLogger(videoData.logAggregator, 'Zoom', {});
}
reset(){
this.scale = 1;
this.scaleY = 1;
this.logScale = 0;
this.logScaleY = 0;
}
/**
* Increases zoom by a given amount. Does not allow per-axis zoom.
* Will set zoom level to x axis (+ given amount) if x and y zooms differ.
* @param amount
* @param axis leave undefined to apply zoom to both axes
*/
zoomStep(amount){
this.logScale += amount;
zoomStep(amount: number, axis?: 'x' | 'y') {
const effectiveLog = {
x: Math.log2(this.effectiveZoom.x),
y: Math.log2(this.effectiveZoom.y)
};
if (this.logScale <= this.minScale) {
this.logScale = this.minScale;
}
if (this.logScale >= this.maxScale) {
this.logScale = this.maxScale;
}
let newLog = axis === 'y' ? effectiveLog.y : effectiveLog.x;
newLog += amount;
newLog = Math.min(Math.max(newLog, LOG_MIN_SCALE), LOG_MAX_SCALE);
this.logScaleY = this.logScale;
// if axis is undefined, both of this statements should trigger)
if (axis !== 'y') {
this.logScale = newLog;
}
if (axis !== 'x') {
this.logScaleY = newLog;
}
this.scale = Math.pow(2, this.logScale);
this.scaleY = Math.pow(2, this.logScaleY);
this.logger.log('info', 'debug', "[Zoom::zoomStep] changing zoom by", amount, ". New zoom level:", this.scale);
this.processZoom();
}
setZoom(scale: number, axis?: 'x' |'y', noAnnounce?){
this.logger.log('info', 'debug', "[Zoom::setZoom] Setting zoom to", scale, "!");
/**
* Sets zoom to specific value
* @param scale
*/
setZoom(scale: number | {x: number, y: number}){
// 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;
}
const scaleIn = (typeof scale === 'number') ?
{
x: scale,
y: scale
} : {
x: scale.x ?? this.scale,
y: scale.y ?? this.scaleY
};
switch (axis) {
case 'x':
this.scale = scale;
break;
case 'y':
this.scaleY = scale;
break;
default:
this.scale = scale;
this.scaleY = scale;
}
this.scale = Math.min(Math.max(scaleIn.x, MIN_SCALE), MAX_SCALE);
this.scaleY = Math.min(Math.max(scaleIn.y, MIN_SCALE), MAX_SCALE);
this.processZoom();
}
processZoom() {
// this.conf.resizer.toFixedAr();
this.conf.resizer.toFixedAr();
this.conf.resizer.applyScaling({xFactor: this.scale, yFactor: this.scaleY}, {noAnnounce: true});
}
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;

View File

@ -5,17 +5,18 @@
import UWContent from './UWContent';
if(process.env.CHANNEL !== 'stable'){
console.warn("\n\n\n\n\n\n ——— Sᴛλʀᴛɪɴɢ Uʟᴛʀᴀɪɪʏ ———\n << ʟᴏᴀᴅɪɴɢ ᴍᴀɪɴ ꜰɪʟᴇ >>\n\n\n\n");
let isIframe;
try {
if(window.self !== window.top){
console.info("%cWe aren't in an iframe.", "color: #afc, background: #174");
}
else{
console.info("%cWe are in an iframe!", "color: #fea, background: #d31", window.self, window.top);
}
isIframe = window.self !== window.top;
} catch (e) {
console.info("%cWe are in an iframe!", "color: #fea, background: #d31");
isIframe = true;
}
console.warn(
"\n\n\n\n\n\n ——— Sᴛλʀᴛɪɴɢ Uʟᴛʀᴀɪɪʏ ———\n << ʟᴏᴀᴅɪɴɢ ᴍᴀɪɴ ꜰɪʟᴇ >>\n\n\n\n",
"\n - are we in iframe?", isIframe
);
}
const main = new UWContent();

View File

@ -53,9 +53,9 @@
</template>
<script>
import Debug from '../../ext/conf/Debug';
import BrowserDetect from '../../ext/conf/BrowserDetect';
import Logger from '../../ext/lib/Logger';
import { LogAggregator } from '@src/ext/lib/logging/LogAggregator';
import { ComponentLogger } from '@src/ext/lib/logging/ComponentLogger';
export default {
data () {
@ -75,6 +75,7 @@ export default {
settings: {},
settingsInitialized: false,
logger: {},
logAggregator: {},
siteTabDisabled: false,
videoTabDisabled: false,
canShowVideoTab: {canShow: true, warning: true},
@ -82,12 +83,10 @@ export default {
}
},
async created() {
this.logger = new Logger();
await this.logger.init({
allowLogging: true,
});
this.logAggregator = new LogAggregator('');
this.logger = new ComponentLogger(this.logAggregator, 'App.vue');
this.settings = new Settings({updateCallback: () => this.updateConfig(), logger: this.logger});
this.settings = new Settings({updateCallback: () => this.updateConfig(), logAggregator: this.logAggregator });
await this.settings.init();
this.settingsInitialized = true;
},

View File

@ -54,9 +54,9 @@
</template>
<script>
import Debug from '../../ext/conf/Debug';
import BrowserDetect from '../../ext/conf/BrowserDetect';
import Logger from '../../ext/lib/Logger';
import { LogAggregator } from '@src/ext/lib/logging/LogAggregator';
import { ComponentLogger } from '@src/ext/lib/logging/ComponentLogger';
export default {
data () {
@ -75,6 +75,7 @@ export default {
currentZoom: 1,
settings: {},
settingsInitialized: false,
logAggregator: {},
logger: {},
siteTabDisabled: false,
videoTabDisabled: false,
@ -83,12 +84,10 @@ export default {
}
},
async created() {
this.logger = new Logger();
await this.logger.init({
allowLogging: true,
});
this.logAggregator = new LogAggregator('');
this.logger = new ComponentLogger(this.logAggregator, 'App.vue');
this.settings = new Settings({updateCallback: () => this.updateConfig(), logger: this.logger});
this.settings = new Settings({updateCallback: () => this.updateConfig(), logAggregator: this.logAggregator});
await this.settings.init();
this.settingsInitialized = true;
},

View File

@ -2,7 +2,7 @@
"manifest_version": 3,
"name": "Ultrawidify",
"description": "Removes black bars on ultrawide videos and offers advanced options to fix aspect ratio.",
"version": "6.2.4",
"version": "6.3.0",
"icons": {
"32":"res/icons/uw-32.png",
"64":"res/icons/uw-64.png"

View File

@ -111,10 +111,6 @@
<script>
import Donate from '../common/misc/Donate.vue';
import SuperAdvancedSettings from './SuperAdvancedSettings.vue';
import Debug from '../ext/conf/Debug';
import BrowserDetect from '../ext/conf/BrowserDetect';
import ExtensionConf from '../ext/conf/ExtensionConf';
import ObjectCopy from '../ext/lib/ObjectCopy';
import Settings from '../ext/lib/Settings';
import GeneralSettings from './GeneralSettings';
import ControlsSettings from './controls-settings/ControlsSettings';
@ -122,8 +118,8 @@ import AddEditActionPopup from './controls-settings/AddEditActionPopup';
import ConfirmPopup from './common/ConfirmationPopup';
import About from './about'
import AutodetectionSettings from './AutodetectionSettings';
// import SuperAdvancedSettings from './'
import Logger from '../ext/lib/Logger';
import { LogAggregator } from '@src/ext/lib/logging/LogAggregator';
import { ComponentLogger } from '@src/ext/lib/logging/ComponentLogger';
export default {
name: "Ultrawidify",
@ -132,6 +128,7 @@ export default {
selectedTab: "general",
selectedTabTitle: "General settings",
settings: {},
logAggregator: {},
logger: {},
settingsInitialized: false,
editActionPopupVisible: false,
@ -145,12 +142,10 @@ export default {
}
},
async created () {
this.logger = new Logger();
await this.logger.init({
allowLogging: true,
});
this.logAggregator = new LogAggregator('');
this.logger = new ComponentLogger(this.logAggregator, 'App.vue');
this.settings = new Settings({updateCallback: this.updateSettings, logger: this.logger});
this.settings = new Settings({updateCallback: this.updateSettings, logAggregator: this.logAggregator});
await this.settings.init();
this.settingsInitialized = true;

View File

@ -601,17 +601,6 @@ export default {
},
created() {
this.sensitivity = this.getSensitivity();
const canvas = document.createElement('canvas');
canvas.width = 10;
canvas.height = 10;
const ctx = canvas.getContext('2d');
try {
ctx.drawWindow(window,0, 0, 10, 10, "rgba(0,0,0,0)");
this.fallbackModeAvailable = true;
} catch (e) {
this.fallbackModeAvailable = false;
}
},
methods: {
setArCheckFrequency(event) {