Compare commits

..

No commits in common. "master" and "feature/player-ui" have entirely different histories.

142 changed files with 7586 additions and 12747 deletions

View File

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

View File

@ -1,63 +1,18 @@
# Changelog # Changelog
### Plans for the future
* Native builds for Chromium Edge
* Settings page looks ugly af right now. Maybe fix it some time later
* other bug fixes
## v7.0 (planned major)
* WebGL autodetection
## v6.0 (current major) ## 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
* Switching between full screen, theater, and normal player now correctly enables and disables the extension according to the settings.
* Don't reset settings if updating conf patches fails.
* Darken in-player UI backgrounds until Chrome fixes its backdrop-filter bug
### v6.2.3
* Fixed default persistent mode
### v6.2.2
* Fixed the issue where stretching was not applied if no cropping was used
* Added way to manually enable or disable color scheme detection, as a compromise between #259 and #264
### v6.2.1
* Fixed offset issue on Netflix
* Fixed issue with overlay iframe not being transparent on twitter
### v6.2.0
* Parts of automatic aspect ratio detection use WebGL now. This should result in improved performance.
* In-player UI now indicates whether a site is having a problem with automatic aspect ratio detection.
* UI improvements
Since automatic aspect ratio detection had to be rewritten completely and since the new version needs to be out 5 days ago, there are some regressions in the automatic aspect ratio detection.
### v6.1.0
Was skipped due to how major the changes were.
### v6.0.1
* Fixed external links
### v6.0.0
Chrome only, because I needed to rush manifest v3 migration before ensuring things work in Firefox. Chrome only, because I needed to rush manifest v3 migration before ensuring things work in Firefox.
* In player UI * In player UI
@ -81,13 +36,6 @@ Firefox-only.
* When cropping and panning video, CSS' `transform: translate(x,y)` now uses integers _always_. Before that fix, `translate(x,y)` could be offset by x.5 px, and that half of a pixel introduced a 1px thick line on the portion of the left edge of the video. * When cropping and panning video, CSS' `transform: translate(x,y)` now uses integers _always_. Before that fix, `translate(x,y)` could be offset by x.5 px, and that half of a pixel introduced a 1px thick line on the portion of the left edge of the video.
### v5.1.7
* When aligning videos, ensure video is never translated by a fractional value
* As of recent nVidia driver update in Edge, Angle detection fails in a way that prevents popup from showing up. This problem should be fixed now.
The angle detection problem was fixed by disabling the angle detection check for now, as Chrome, et al. appear to have fixed buggy video hardware acceleration.
### v5.1.6 ### v5.1.6
* Added new config for disney+ (courtesy of @MStefan99) * Added new config for disney+ (courtesy of @MStefan99)

View File

@ -1,39 +0,0 @@
# 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.

View File

@ -1,35 +0,0 @@
# 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.

434
package-lock.json generated
View File

@ -1,6 +1,6 @@
{ {
"name": "ultrawidify", "name": "ultrawidify",
"version": "6.3.0", "version": "6.0.0",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@ -28,6 +28,7 @@
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
"integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
"dev": true,
"requires": { "requires": {
"@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.24" "@jridgewell/trace-mapping": "^0.3.24"
@ -1375,109 +1376,6 @@
"to-fast-properties": "^2.0.0" "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": { "@hapi/address": {
"version": "2.1.4", "version": "2.1.4",
"resolved": "https://registry.npmjs.org/@hapi/address/-/address-2.1.4.tgz", "resolved": "https://registry.npmjs.org/@hapi/address/-/address-2.1.4.tgz",
@ -1557,57 +1455,6 @@
"@jridgewell/sourcemap-codec": "^1.4.14" "@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": { "@mdi/font": {
"version": "6.9.96", "version": "6.9.96",
"resolved": "https://registry.npmjs.org/@mdi/font/-/font-6.9.96.tgz", "resolved": "https://registry.npmjs.org/@mdi/font/-/font-6.9.96.tgz",
@ -1735,27 +1582,12 @@
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
"dev": true "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": { "@sindresorhus/is": {
"version": "0.7.0", "version": "0.7.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.7.0.tgz", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.7.0.tgz",
"integrity": "sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow==", "integrity": "sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow==",
"dev": true "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": { "@types/accepts": {
"version": "1.3.7", "version": "1.3.7",
"resolved": "https://registry.npmjs.org/@types/accepts/-/accepts-1.3.7.tgz", "resolved": "https://registry.npmjs.org/@types/accepts/-/accepts-1.3.7.tgz",
@ -1839,11 +1671,6 @@
"es6-promise": "*" "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": { "@types/express": {
"version": "4.17.21", "version": "4.17.21",
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz",
@ -3205,11 +3032,6 @@
"sprintf-js": "~1.0.2" "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": { "arr-diff": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz",
@ -3553,11 +3375,6 @@
"integrity": "sha512-3AungXC4I8kKsS9PuS4JH2nc+0bVY/mjgrephHTIi8fpEeGsTHBUJeosp0Wc1myYMElmD0B3Oc4XL/HVJ4PV2g==", "integrity": "sha512-3AungXC4I8kKsS9PuS4JH2nc+0bVY/mjgrephHTIi8fpEeGsTHBUJeosp0Wc1myYMElmD0B3Oc4XL/HVJ4PV2g==",
"dev": true "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": { "babel-code-frame": {
"version": "6.26.0", "version": "6.26.0",
"resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz",
@ -3979,16 +3796,6 @@
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
"dev": true "dev": true
}, },
"bindings": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
"dev": true,
"optional": true,
"requires": {
"file-uri-to-path": "1.0.0"
}
},
"bl": { "bl": {
"version": "1.2.3", "version": "1.2.3",
"resolved": "https://registry.npmjs.org/bl/-/bl-1.2.3.tgz", "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.3.tgz",
@ -4943,11 +4750,6 @@
"mimic-response": "^1.0.0" "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": { "cmd-shim": {
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/cmd-shim/-/cmd-shim-3.0.3.tgz", "resolved": "https://registry.npmjs.org/cmd-shim/-/cmd-shim-3.0.3.tgz",
@ -4973,11 +4775,6 @@
"integrity": "sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==", "integrity": "sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==",
"dev": true "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": { "collection-visit": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz",
@ -5559,11 +5356,6 @@
"sha.js": "^2.4.8" "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": { "cross-env": {
"version": "5.2.1", "version": "5.2.1",
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-5.2.1.tgz", "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-5.2.1.tgz",
@ -6213,11 +6005,6 @@
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
"dev": true "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": { "diffie-hellman": {
"version": "5.0.3", "version": "5.0.3",
"resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz",
@ -6569,25 +6356,12 @@
"estraverse": "^4.1.1" "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": { "esprima": {
"version": "4.0.1", "version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
"dev": true "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": { "esrecurse": {
"version": "4.3.0", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
@ -6947,7 +6721,8 @@
"fast-deep-equal": { "fast-deep-equal": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true
}, },
"fast-glob": { "fast-glob": {
"version": "2.2.7", "version": "2.2.7",
@ -6969,11 +6744,6 @@
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
"dev": true "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": { "fastparse": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.2.tgz", "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.2.tgz",
@ -7041,13 +6811,6 @@
"integrity": "sha512-qyQ0pzAy78gVoJsmYeNgl8uH8yKhr1lVhW7JbzJmnlRi0I4R2eEDEJZVKG8agpDnLpacwNbDhLNG/LMdxHD2YQ==", "integrity": "sha512-qyQ0pzAy78gVoJsmYeNgl8uH8yKhr1lVhW7JbzJmnlRi0I4R2eEDEJZVKG8agpDnLpacwNbDhLNG/LMdxHD2YQ==",
"dev": true "dev": true
}, },
"file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
"dev": true,
"optional": true
},
"filename-reserved-regex": { "filename-reserved-regex": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz", "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz",
@ -7722,11 +7485,6 @@
} }
} }
}, },
"gl-matrix": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.3.tgz",
"integrity": "sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA=="
},
"glob": { "glob": {
"version": "7.2.3", "version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
@ -8335,11 +8093,6 @@
"integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==",
"dev": true "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": { "import-fresh": {
"version": "3.3.0", "version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@ -8843,14 +8596,6 @@
"integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==",
"dev": true "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": { "is-regex": {
"version": "1.1.4", "version": "1.1.4",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz",
@ -9003,11 +8748,6 @@
"integrity": "sha512-fnjC0up+0SjEJtgmmG+teeel68kutkvzfctO/KxE3qJlbunkJYAshgH3boU++gSBHP8z5/r0ts0qRIrHf0RTQQ==", "integrity": "sha512-fnjC0up+0SjEJtgmmG+teeel68kutkvzfctO/KxE3qJlbunkJYAshgH3boU++gSBHP8z5/r0ts0qRIrHf0RTQQ==",
"dev": true "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": { "js-base64": {
"version": "2.6.4", "version": "2.6.4",
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.6.4.tgz", "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.6.4.tgz",
@ -9103,11 +8843,6 @@
} }
} }
}, },
"jsep": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/jsep/-/jsep-1.4.0.tgz",
"integrity": "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw=="
},
"jsesc": { "jsesc": {
"version": "2.5.2", "version": "2.5.2",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
@ -9147,11 +8882,6 @@
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"dev": true "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": { "json-stringify-safe": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
@ -9174,21 +8904,6 @@
"graceful-fs": "^4.1.6" "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": { "jsprim": {
"version": "1.4.2", "version": "1.4.2",
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz",
@ -9293,11 +9008,6 @@
"json5": "^1.0.1" "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": { "locate-path": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
@ -9312,11 +9022,6 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" "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": { "lodash.camelcase": {
"version": "4.3.0", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
@ -9536,11 +9241,6 @@
"fs-monkey": "^1.0.4" "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": { "memory-fs": {
"version": "0.5.0", "version": "0.5.0",
"resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.5.0.tgz", "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.5.0.tgz",
@ -9841,11 +9541,6 @@
"to-regex": "^3.0.1" "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": { "ndjson": {
"version": "1.5.0", "version": "1.5.0",
"resolved": "https://registry.npmjs.org/ndjson/-/ndjson-1.5.0.tgz", "resolved": "https://registry.npmjs.org/ndjson/-/ndjson-1.5.0.tgz",
@ -13615,11 +13310,6 @@
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==" "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": { "require-main-filename": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
@ -14686,11 +14376,6 @@
"escape-string-regexp": "^1.0.2" "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": { "subscriptions-transport-ws": {
"version": "0.9.19", "version": "0.9.19",
"resolved": "https://registry.npmjs.org/subscriptions-transport-ws/-/subscriptions-transport-ws-0.9.19.tgz", "resolved": "https://registry.npmjs.org/subscriptions-transport-ws/-/subscriptions-transport-ws-0.9.19.tgz",
@ -14717,47 +14402,6 @@
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==" "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": { "svgo": {
"version": "0.7.2", "version": "0.7.2",
"resolved": "https://registry.npmjs.org/svgo/-/svgo-0.7.2.tgz", "resolved": "https://registry.npmjs.org/svgo/-/svgo-0.7.2.tgz",
@ -15809,65 +15453,6 @@
"builtins": "^1.0.3" "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": { "vary": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
@ -16344,11 +15929,6 @@
"resolved": "https://registry.npmjs.org/vuex-webextensions/-/vuex-webextensions-1.3.3.tgz", "resolved": "https://registry.npmjs.org/vuex-webextensions/-/vuex-webextensions-1.3.3.tgz",
"integrity": "sha512-Qz+KmF4CYLfomAIuEjY4A9DYqzwXZo18TkvTyO84DkW/s274iI2IyuOSFZoLcVHMS5BjUhRbyFclqqAwz58zgw==" "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": { "watch": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/watch/-/watch-1.0.2.tgz", "resolved": "https://registry.npmjs.org/watch/-/watch-1.0.2.tgz",
@ -16439,7 +16019,6 @@
"dev": true, "dev": true,
"optional": true, "optional": true,
"requires": { "requires": {
"bindings": "^1.5.0",
"nan": "^2.12.1" "nan": "^2.12.1"
} }
}, },
@ -16986,11 +16565,6 @@
"zen-observable": "^0.8.0" "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": { "zip-stream": {
"version": "2.1.3", "version": "2.1.3",
"resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-2.1.3.tgz", "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-2.1.3.tgz",

View File

@ -1,6 +1,6 @@
{ {
"name": "ultrawidify", "name": "ultrawidify",
"version": "6.3.0", "version": "6.0.0",
"description": "Aspect ratio fixer for youtube and other sites, with automatic aspect ratio detection. Supports ultrawide and other ratios.", "description": "Aspect ratio fixer for youtube and other sites, with automatic aspect ratio detection. Supports ultrawide and other ratios.",
"author": "Tamius Han <tamius.han@gmail.com>", "author": "Tamius Han <tamius.han@gmail.com>",
"scripts": { "scripts": {
@ -31,12 +31,10 @@
"@types/resize-observer-browser": "^0.1.6", "@types/resize-observer-browser": "^0.1.6",
"concurrently": "^5.3.0", "concurrently": "^5.3.0",
"fs-extra": "^7.0.1", "fs-extra": "^7.0.1",
"gl-matrix": "^3.4.3",
"json-cyclic": "0.0.3", "json-cyclic": "0.0.3",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"mdi-vue": "^3.0.11", "mdi-vue": "^3.0.11",
"typescript": "^4.4.4", "typescript": "^4.4.4",
"vanilla-jsoneditor": "^3.3.0",
"vue": "^3.2.21", "vue": "^3.2.21",
"vue-style-loader": "^4.1.3", "vue-style-loader": "^4.1.3",
"vuex": "^4.0.2", "vuex": "^4.0.2",

View File

@ -5,7 +5,7 @@
# #
# Script assumes we're in basedir of the repository and that extension has been built and zipped to /dist-zip # Script assumes we're in basedir of the repository and that extension has been built and zipped to /dist-zip
# using the crx name we want (same name as zip, except different extension) # using the crx name we want (same name as zip, except different extension)
# #
# also this doesn't check for errors ever so # also this doesn't check for errors ever so
# #
@ -42,4 +42,4 @@ sig_len_hex=$(byte_swap $(printf '%08x\n' $(ls -l "$sig" | awk '{print $5}')))
echo "Wrote $crx" echo "Wrote $crx"
echo "exiting dist-zip" echo "exiting dist-zip"
cd .. cd ..

View File

@ -24,7 +24,7 @@ const buildZip = (src, dist, zipFilename) => {
const archive = archiver('zip', { zlib: { level: 9 }}); const archive = archiver('zip', { zlib: { level: 9 }});
const stream = fs.createWriteStream(path.join(dist, zipFilename)); const stream = fs.createWriteStream(path.join(dist, zipFilename));
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
archive archive
.directory(src, false) .directory(src, false)
@ -46,14 +46,14 @@ const main = () => {
browserPostfix = browser; browserPostfix = browser;
} }
const destDir = path.join(__dirname, `../dist-${browserPostfix}`); const destDir = path.join(__dirname, `../dist-${browserPostfix}`);
const zipDir = path.join(__dirname, '../dist-zip'); const zipDir = path.join(__dirname, '../dist-zip');
const {name, version} = extractExtensionData(browserPostfix); const {name, version} = extractExtensionData(browserPostfix);
// collapse spaces and dashes into single dash // collapse spaces and dashes into single dash
const baseFilename = `${name.replace(/[ -]+/g, '-')}-${version}`; const baseFilename = `${name.replace(/[ -]+/g, '-')}-${version}`;
let realZipDir; let realZipDir;
if (!!testingOrNightly) { if (!!testingOrNightly) {
realZipDir = path.join(zipDir, version); realZipDir = path.join(zipDir, version);
} else { } else {
@ -61,7 +61,7 @@ const main = () => {
} }
const zipFilename = `${baseFilename}-${browser}.zip`; const zipFilename = `${baseFilename}-${browser}.zip`;
try { try {
makeDirIfNotExists(realZipDir, {recursive: true}); makeDirIfNotExists(realZipDir, {recursive: true});
} catch (e) { } catch (e) {
@ -70,7 +70,7 @@ const main = () => {
} }
buildZip(destDir, realZipDir, zipFilename) buildZip(destDir, realZipDir, zipFilename)
.then(() => console.info('OK')) .then(() => console.info('OK'))
.catch(console.err); .catch(console.err);
}; };
main(); main();

View File

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

View File

@ -6,19 +6,6 @@ import ExtensionMode from '../enums/ExtensionMode.enum'
import StretchType from '../enums/StretchType.enum' import StretchType from '../enums/StretchType.enum'
import VideoAlignmentType from '../enums/VideoAlignmentType.enum' import VideoAlignmentType from '../enums/VideoAlignmentType.enum'
export enum ExtensionEnvironment {
Normal = 'normal',
Theater = 'theater',
Fullscreen = 'fullscreen',
}
export interface DevUiConfig {
aardDebugOverlay: {
showOnStartup: boolean,
showDetectionDetails: boolean,
}
}
export interface KeyboardShortcutInterface { export interface KeyboardShortcutInterface {
key?: string, key?: string,
code?: string, code?: string,
@ -64,141 +51,139 @@ export interface CommandInterface {
export type SettingsReloadComponent = 'PlayerData' | 'VideoData'; export type SettingsReloadComponent = 'PlayerData' | 'VideoData';
export type SettingsReloadFlags = true | SettingsReloadComponent; export type SettingsReloadFlags = true | SettingsReloadComponent;
export interface AardSettings {
aardType: 'webgl' | 'legacy' | 'auto';
earlyStopOptions: {
stopAfterFirstDetection: boolean;
stopAfterTimeout: boolean;
stopTimeout: number;
},
disabledReason: string, // if automatic aspect ratio has been disabled, show reason
allowedMisaligned: number, // top and bottom letterbox thickness can differ by this much.
// Any more and we don't adjust ar.
allowedArVariance: number, // amount by which old ar can differ from the new (1 = 100%)
timers: { // autodetection frequency
playing: number, // while playing
playingReduced: number, // while video/player element has insufficient size
paused: number, // while paused
error: number, // after error
minimumTimeout: number,
tickrate: number, // 1 tick every this many milliseconds
},
autoDisable: { // settings for automatically disabling the extension
maxExecutionTime: number, // if execution time of main autodetect loop exceeds this many milliseconds,
// we disable it.
consecutiveTimeoutCount: number, // we only do it if it happens this many consecutive times
// FOR FUTURE USE
consecutiveArResets: number // if aspect ratio reverts immediately after AR change is applied, we disable everything
},
canvasDimensions: {
blackframeCanvas: { // smaller than sample canvas, blackframe canvas is used to recon for black frames
// it's not used to detect aspect ratio by itself, so it can be tiny af
width: number,
height: number,
},
sampleCanvas: { // size of image sample for detecting aspect ratio. Bigger size means more accurate results,
// at the expense of performance
width: number,
height: number,
},
},
blackLevels: {
defaultBlack: number, // By default, pixels darker than this are considered black.
// (If detection algorithm detects darker blacks, black is considered darkest detected pixel)
blackTolerance: number, // If pixel is more than this much brighter than blackLevel, it's considered not black
// It is not considered a valid image detection if gradient detection is enabled
imageDelta: number, // When gradient detection is enabled, pixels this much brighter than black skip gradient detection
}
sampling: {
edgePosition: number; // % of width (max 0.33). Pixels up to this far away from either edge may contain logo.
staticCols: number, // we take a column at [0-n]/n-th parts along the width and sample it
randomCols: number, // we add this many randomly selected columns to the static columns
staticRows: number, // forms grid with staticSampleCols. Determined in the same way. For black frame checks,
},
// pls deprecate and move things used
edgeDetection: {
slopeTestWidth: number,
gradientTestSamples: number, // we check this many pixels below (or above) the suspected edge to check for gradient
gradientTestBlackThreshold: number, // if pixel in test sample is brighter than that, we aren't looking at gradient
gradientTestDeltaThreshold: number, // if delta between two adjacent pixels in gradient test exceeds this, it's not gradient
gradientTestMinDelta: number, // if last pixels of the test sample is less than this brighter than the first -> not gradient
thresholds: {
edgeDetectionLimit: number, // during scanning of the edge, quit after edge gets detected at this many points
minQualitySingleEdge: number, // At least one of the detected must reach this quality
minQualitySecondEdge: number, // The other edge must reach this quality (must be smaller or equal to single edge quality)
}
maxLetterboxOffset: number, // Upper and lower letterbox can be different by this many (% of height)
// Previous iteration variables VVVV
sampleWidth: number, // we take a sample this wide for edge detection
detectionThreshold: number, // sample needs to have this many non-black pixels to be a valid edge
confirmationThreshold: number, //
singleSideConfirmationThreshold: number, // we need this much edges (out of all samples, not just edges) in order
// to confirm an edge in case there's no edges on top or bottom (other
// than logo, of course)
logoThreshold: number, // if edge candidate sits with count greater than this*all_samples, it can't be logo
// or watermark.
edgeTolerancePx?: number, // we check for black edge violation this far from detection point
edgeTolerancePercent?: number, // we check for black edge detection this % of height from detection point. unused
middleIgnoredArea: number, // we ignore this % of canvas height towards edges while detecting aspect ratios
minColsForSearch: number, // if we hit the edge of blackbars for all but this many columns (%-wise), we don't
// continue with search. It's pointless, because black edge is higher/lower than we
// are now. (NOTE: keep this less than 1 in case we implement logo detection)
edgeMismatchTolerancePx: number,// corners and center are considered equal if they differ by at most this many px
},
pillarTest: {
ignoreThinPillarsPx: number, // ignore pillars that are less than this many pixels thick.
allowMisaligned: number // left and right edge can vary this much (%)
},
textLineTest: {
nonTextPulse: number, // if a single continuous pulse has this many non-black pixels, we aren't dealing
// with text. This value is relative to canvas width (%)
pulsesToConfirm: number, // this is a threshold to confirm we're seeing text.
pulsesToConfirmIfHalfBlack: number, // this is the threshold to confirm we're seeing text if longest black pulse
// is over 50% of the canvas width
testRowOffset: number // we test this % of height from detected edge
}
}
interface DevSettings {
loadFromSnapshot: boolean,
}
interface SettingsInterface { interface SettingsInterface {
_updateFlags?: { _updateFlags?: {
requireReload?: SettingsReloadFlags, requireReload?: SettingsReloadFlags,
forSite?: string forSite?: string
} }
dev: DevSettings,
arDetect: AardSettings, arDetect: {
disabledReason: string, // if automatic aspect ratio has been disabled, show reason
allowedMisaligned: number, // top and bottom letterbox thickness can differ by this much.
// Any more and we don't adjust ar.
allowedArVariance: number, // amount by which old ar can differ from the new (1 = 100%)
timers: { // autodetection frequency
playing: number, // while playing
paused: number, // while paused
error: number, // after error
minimumTimeout: number,
tickrate: number, // 1 tick every this many milliseconds
},
autoDisable: { // settings for automatically disabling the extension
maxExecutionTime: number, // if execution time of main autodetect loop exceeds this many milliseconds,
// we disable it.
consecutiveTimeoutCount: number, // we only do it if it happens this many consecutive times
ui: { // FOR FUTURE USE
inPlayer: { consecutiveArResets: number // if aspect ratio reverts immediately after AR change is applied, we disable everything
enabled: boolean, // Deprecated — moved to site settings },
enabledFullscreenOnly: boolean, // Deprecated — moved to site settings canvasDimensions: {
popupAlignment: 'left' | 'right', blackframeCanvas: { // smaller than sample canvas, blackframe canvas is used to recon for black frames
minEnabledWidth: number, // don't show UI if player is narrower than % of screen width // it's not used to detect aspect ratio by itself, so it can be tiny af
minEnabledHeight: number, // don't show UI if player is narrower than % of screen height width: number,
activation: 'trigger-zone' | 'player', // what needs to be hovered in order for UI to be visible height: number,
triggerZoneDimensions: { // how large the trigger zone is (relative to player size) },
width: number sampleCanvas: { // size of image sample for detecting aspect ratio. Bigger size means more accurate results,
// at the expense of performance
width: number,
height: number, height: number,
offsetX: number, // fed to translateX(offsetX + '%'). Valid range [-100, 0]
offsetY: number // fed to translateY(offsetY + '%'). Valid range [-100, 100]
}, },
}, },
devMode?: boolean,
dev: DevUiConfig, // samplingInterval: 10, // we sample at columns at (width/this) * [ 1 .. this - 1]
} blackframe: {
sufficientColorVariance: number, // calculate difference between average intensity and pixel, for every pixel for every color
// component. Average intensity is normalized to where 0 is black and 1 is biggest value for
// that component. If sum of differences between normalized average intensity and normalized
// component varies more than this % between color components, we can afford to use less strict
// cumulative threshold.
cumulativeThresholdLax: number,
cumulativeThresholdStrict: number,// if we add values of all pixels together and get more than this, the frame is bright enough.
// (note: blackframe is 16x9 px -> 144px total. cumulative threshold can be reached fast)
blackPixelsCondition: number, // How much pixels must be black (1 all, 0 none) before we consider frame as black. Takes
// precedence over cumulative threshold: if blackPixelsCondition is met, the frame is dark
// regardless of whether cumulative threshold has been reached.
},
blackbar: {
blackLevel: number, // everything darker than 10/255 across all RGB components is considered black by
// default. blackLevel can decrease if we detect darker black.
threshold: number, // if pixel is darker than the sum of black level and this value, we count it as black
// on 0-255. Needs to be fairly high (8 might not cut it) due to compression
// artifacts in the video itself
frameThreshold: number, // threshold, but when doing blackframe test
imageThreshold: number, // in order to detect pixel as "not black", the pixel must be brighter than
// the sum of black level, threshold and this value.
gradientThreshold: number, // When trying to determine thickness of the black bars, we take 2 values: position of
// the last pixel that's darker than our threshold, and position of the first pixel that's
// brighter than our image threshold. If positions are more than this many pixels apart,
// we assume we aren't looking at letterbox and thus don't correct the aspect ratio.
gradientSampleSize: number, // How far do we look to find the gradient
maxGradient: number, // if two neighboring pixels in gradientSampleSize differ by more than this, then we aren't
// looking at a gradient
gradientNegativeTreshold: number,
gradientMaxSD: number, // reserved for future use
antiGradientMode: AntiGradientMode
},
variableBlackbarThresholdOptions: { // In case of poor bitrate videos, jpeg artifacts may cause us issues
// FOR FUTURE USE
enabled: boolean, // allow increasing blackbar threshold
disableArDetectOnMax: boolean, // disable autodetection when threshold goes over max blackbar threshold
maxBlackbarThreshold: number, // max threshold (don't increase past this)
thresholdStep: number, // when failing to set aspect ratio, increase threshold by this much
increaseAfterConsecutiveResets: number // increase if AR resets this many times in a row
},
sampling: {
staticCols: number, // we take a column at [0-n]/n-th parts along the width and sample it
randomCols: number, // we add this many randomly selected columns to the static columns
staticRows: number, // forms grid with staticSampleCols. Determined in the same way. For black frame checks
},
guardLine: { // all pixels on the guardline need to be black, or else we trigger AR recalculation
// (if AR fails to be recalculated, we reset AR)
enabled: boolean,
ignoreEdgeMargin: number, // we ignore anything that pokes over the black line this close to the edge
// (relative to width of the sample)
imageTestThreshold: number, // when testing for image, this much pixels must be over blackbarThreshold
edgeTolerancePx: number, // black edge violation is performed this far from reported 'last black pixel'
edgeTolerancePercent: null // unused. same as above, except use % of canvas height instead of pixels
},
fallbackMode: {
enabled: boolean,
safetyBorderPx: number, // determines the thickness of safety border in fallback mode
noTriggerZonePx: number // if we detect edge less than this many pixels thick, we don't correct.
},
arSwitchLimiter: { // to be implemented
switches: number, // we can switch this many times
period: number // per this period
},
edgeDetection: {
sampleWidth: number, // we take a sample this wide for edge detection
detectionThreshold: number, // sample needs to have this many non-black pixels to be a valid edge
confirmationThreshold: number, //
singleSideConfirmationThreshold: number, // we need this much edges (out of all samples, not just edges) in order
// to confirm an edge in case there's no edges on top or bottom (other
// than logo, of course)
logoThreshold: number, // if edge candidate sits with count greater than this*all_samples, it can't be logo
// or watermark.
edgeTolerancePx?: number, // we check for black edge violation this far from detection point
edgeTolerancePercent?: number, // we check for black edge detection this % of height from detection point. unused
middleIgnoredArea: number, // we ignore this % of canvas height towards edges while detecting aspect ratios
minColsForSearch: number, // if we hit the edge of blackbars for all but this many columns (%-wise), we don't
// continue with search. It's pointless, because black edge is higher/lower than we
// are now. (NOTE: keep this less than 1 in case we implement logo detection)
},
pillarTest: {
ignoreThinPillarsPx: number, // ignore pillars that are less than this many pixels thick.
allowMisaligned: number // left and right edge can vary this much (%)
},
textLineTest: {
nonTextPulse: number, // if a single continuous pulse has this many non-black pixels, we aren't dealing
// with text. This value is relative to canvas width (%)
pulsesToConfirm: number, // this is a threshold to confirm we're seeing text.
pulsesToConfirmIfHalfBlack: number, // this is the threshold to confirm we're seeing text if longest black pulse
// is over 50% of the canvas width
testRowOffset: number // we test this % of height from detected edge
}
},
restrictions?: RestrictionsSettings; restrictions?: RestrictionsSettings;
@ -245,7 +230,6 @@ interface SettingsInterface {
pan?: any, pan?: any,
version?: string, version?: string,
preventReload?: boolean, preventReload?: boolean,
lastModified?: Date,
// ----------------------------------------- // -----------------------------------------
// ::: MITIGATIONS ::: // ::: MITIGATIONS :::
@ -258,6 +242,33 @@ interface SettingsInterface {
limit?: number, 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 // 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. // complicated way. Hopefully it'll be easier to maintain it that way.
commands?: { commands?: {
@ -268,7 +279,6 @@ interface SettingsInterface {
internal?: CommandInterface[], internal?: CommandInterface[],
}, },
whatsNewChecked: boolean, whatsNewChecked: boolean,
newFeatureTracker: any,
// ----------------------------------------- // -----------------------------------------
// ::: SITE CONFIGURATION ::: // ::: SITE CONFIGURATION :::
// ----------------------------------------- // -----------------------------------------
@ -300,15 +310,59 @@ interface SettingsInterface {
sites: { sites: {
[x: string]: SiteSettingsInterface, [x: string]: SiteSettingsInterface,
} }
// sites: {
// [x: string]: {
// defaultCrop?: any, // v6 new
// defaultStretch?: any, // v6 new
// enabled: ExtensionEnvironmentSettingsInterface, // v6 new
// enabledAard: ExtensionEnvironmentSettingsInterface,// v6 new
// // everything 'superseded by' needs to be implemented
// // as well as ported from the old settings
// mode?: ExtensionMode, // v6 — superseded by looking at enableIn
// autoar?: ExtensionMode, // v6 — superseded by looking at enableIn
// autoarFallback?: ExtensionMode, // v6 — deprecated, no replacement
// stretch?: StretchType, // v6 — superseded by defaultStretch
// videoAlignment?: VideoAlignmentType,
// keyboardShortcutsEnabled?: ExtensionMode,
// type?: string,
// override?: boolean,
// arPersistence?: boolean,
// actions?: any;
// cropModePersistence?: CropModePersistence;
// DOM?: {
// player?: {
// manual?: boolean,
// querySelectors?: string,
// additionalCss?: string,
// useRelativeAncestor?: boolean,
// videoAncestor?: any,
// playerNodeCss?: string,
// periodicallyRefreshPlayerElement?: boolean
// },
// video?: {
// manual?: boolean,
// querySelectors?: string,
// additionalCss?: string,
// useRelativeAncestor?: boolean,
// playerNodeCss?: string
// }
// },
// css?: string;
// usePlayerArInFullscreen?: boolean;
// restrictions?: RestrictionsSettings;
// }
// }
} }
export interface SiteSettingsInterface { export interface SiteSettingsInterface {
enable: ExtensionEnvironmentSettingsInterface; enable: ExtensionEnvironmentSettingsInterface;
enableAard: ExtensionEnvironmentSettingsInterface; enableAard: ExtensionEnvironmentSettingsInterface;
enableKeyboard: ExtensionEnvironmentSettingsInterface; enableKeyboard: ExtensionEnvironmentSettingsInterface;
enableUI: ExtensionEnvironmentSettingsInterface; // Lies! enableUI doesn't use 'theater' property (but uses the other two)
autocreated?: boolean;
type?: 'official' | 'community' | 'user-defined' | 'testing' | 'officially-disabled' | 'unknown' | 'modified'; type?: 'official' | 'community' | 'user-defined' | 'testing' | 'officially-disabled' | 'unknown' | 'modified';
defaultType: 'official' | 'community' | 'user-defined' | 'testing' | 'officially-disabled' | 'unknown' | 'modified'; defaultType: 'official' | 'community' | 'user-defined' | 'testing' | 'officially-disabled' | 'unknown' | 'modified';
@ -317,7 +371,7 @@ export interface SiteSettingsInterface {
defaults?: { // must be defined in @global and @empty defaults?: { // must be defined in @global and @empty
crop?: {type: AspectRatioType, [x: string]: any}, crop?: {type: AspectRatioType, [x: string]: any},
stretch?: {type: StretchType, ratio?: number}, stretch?: StretchType,
alignment?: {x: VideoAlignmentType, y: VideoAlignmentType}, alignment?: {x: VideoAlignmentType, y: VideoAlignmentType},
} }
@ -325,7 +379,6 @@ export interface SiteSettingsInterface {
stretchModePersistence?: CropModePersistence; stretchModePersistence?: CropModePersistence;
alignmentPersistence?: CropModePersistence; alignmentPersistence?: CropModePersistence;
playerAutoConfig?: PlayerAutoConfigInterface;
activeDOMConfig?: string; activeDOMConfig?: string;
DOMConfig?: { [x: string]: SiteDOMSettingsInterface }; DOMConfig?: { [x: string]: SiteDOMSettingsInterface };
@ -338,12 +391,6 @@ export interface SiteSettingsInterface {
override?: boolean; // whether settings for this site will be overwritten by extension upgrade script override?: boolean; // whether settings for this site will be overwritten by extension upgrade script
} }
export interface PlayerAutoConfigInterface {
modified: boolean;
initialIndex: number;
currentIndex: number;
}
export interface SiteDOMSettingsInterface { export interface SiteDOMSettingsInterface {
type: 'official' | 'community' | 'user-defined' | 'modified' | undefined; type: 'official' | 'community' | 'user-defined' | 'modified' | undefined;
elements?: { elements?: {

View File

@ -1,7 +0,0 @@
import StretchType from '../enums/StretchType.enum';
export interface Stretch {
type: StretchType,
ratio?: number,
limit?: number,
}

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="uw-clickthrough relative w-full h-full"> <div class="uw-clickthrough relative w-100 h-100">
<template v-for="rectangle of drawnRectangles" :key="rectangle.id ?? rectangle"> <template v-for="rectangle of drawnRectangles" :key="rectangle.id ?? rectangle">
<!-- Player element overlays --> <!-- Player element overlays -->
@ -44,6 +44,13 @@ export default {
}, },
async created() { 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. * 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 * We can handle events with the same function we use to handle events from

View File

@ -1,176 +1,21 @@
<template> <template>
<div
class="context-spawn uw-ui-area"
style="z-index: 1000;"
v-if="!triggerZoneEditorVisible"
>
<div
class="spawn-container uw-ui-trigger"
:style="triggerZoneStyles"
>
&nbsp;
</div>
</div>
<div <div
v-if="contextMenuActive || settingsInitialized && uwTriggerZoneVisible && !isGlobal" v-if="settingsInitialized && uwTriggerZoneVisible && !isGlobal"
class="context-spawn uw-ui-area" class="uw-hover uv-hover-trigger-region uw-clickable"
style="z-index: 1001" :style="uwTriggerRegionConf"
@mouseenter="showUwWindow"
> >
<GhettoContextMenu <h1>Aspect ratio controls</h1>
alignment="right" class="uw-menu" <div>Hover to activate</div>
@mouseenter="() => {preventContextMenuHide(); newFeatureViewUpdate('uw6.ui-popup')}"
@mouseleave="allowContextMenuHide()"
>
<template v-slot:activator>
Ultrawidify
</template>
<slot>
<!--
Didn't manage to ensure that extension status pops up above other menu items in less than 3 minutes with z-index,
so wrapping 'status' and 'real menu items' in two different divs, ordering them in the opposite way, and then
ensuring correct ordering with flex-direction: column-reverse ended up being easier and faster.
-->
<div class="uw-clickable menu-width flex-reverse-order">
<div style="z-index: 1000">
<GhettoContextMenu alignment="right">
<template v-slot:activator>
Crop
</template>
<slot>
<GhettoContextMenuOption
v-for="(command, index) of settings?.active.commands.crop"
:key="index"
:label="command.label"
:shortcut="getKeyboardShortcutLabel(command)"
@click="execAction(command)"
>
</GhettoContextMenuOption>
</slot>
</GhettoContextMenu>
<GhettoContextMenu alignment="right">
<template v-slot:activator>
Stretch
</template>
<slot>
<GhettoContextMenuOption
v-for="(command, index) of settings?.active.commands.stretch"
:key="index"
:label="command.label"
:shortcut="getKeyboardShortcutLabel(command)"
@click="execAction(command)"
>
</GhettoContextMenuOption>
</slot>
</GhettoContextMenu>
<GhettoContextMenu alignment="right">
<template v-slot:activator>
Zoom
</template>
<slot>
<GhettoContextMenuOption
v-for="(command, index) of settings?.active.commands.zoom"
:key="index"
:label="command.label"
:shortcut="getKeyboardShortcutLabel(command)"
@click="execAction(command)"
>
</GhettoContextMenuOption>
<GhettoContextMenuItem
:disableHover="true"
>
<ZoomControl
:settings="settings"
:eventBus="eventBus"
/>
</GhettoContextMenuItem>
</slot>
</GhettoContextMenu>
<GhettoContextMenu alignment="right">
<template v-slot:activator>
<div class="context-item">
Align
</div>
</template>
<slot>
<GhettoContextMenuItem :disableHover="true" :css="{'reduced-padding': true}">
<AlignmentOptionsControlComponent
:eventBus="eventBus"
>
</AlignmentOptionsControlComponent>
</GhettoContextMenuItem>
</slot>
</GhettoContextMenu>
<!-- shortcut for configuring UI -->
<GhettoContextMenuOption
v-if="settings.active.newFeatureTracker?.['uw6.ui-popup']?.show > 0"
@click="showUwWindow('playerUiSettings')"
>
<span style="color: #fa6;">Change when and if<br/>this popup appears</span>
<span style="font-size: 0.8rem">
<span style="font-size: 0.8rem; opacity: 0.5">This menu option will show {{settings.active.newFeatureTracker?.['uw6.ui-popup']?.show}} more<br/> times; or until clicked or dismissed.<br/>
Also accessible via:<br/> <span style="font-size: 0.85em">EXTENSION SETTINGS > UI AND KEYBOARD</span>.
</span>
<br/>
<a style="color: #fa6; cursor: pointer;" @click="() => acknowledgeNewFeature('uw6.ui-popup')">Dismiss this option</a>
</span>
</GhettoContextMenuOption>
<!-- -->
<GhettoContextMenuOption
@click="showUwWindow()"
label="Extension settings"
>
</GhettoContextMenuOption>
<GhettoContextMenuOption
@click="showUwWindow('playerDetection')"
label="Incorrect cropping?"
>
</GhettoContextMenuOption>
<GhettoContextMenuOption
@click="showUwWindow('about')"
label="Not working?"
>
</GhettoContextMenuOption>
</div>
<div style="z-index: 10000">
<GhettoContextMenuItem
class="extension-status-messages"
:disableHover="true"
>
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 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/>
by site/browser (CORS).
</div>
<div v-else-if="statusFlags.aardErrors?.webglError" class="aard-blocked">
Autodetection unavailable<br/>
(webgl error)
</div>
</GhettoContextMenuItem>
</div>
</div>
</slot>
</GhettoContextMenu>
</div> </div>
<div <div
v-if="settingsInitialized && uwWindowVisible" v-if="settingsInitialized && uwWindowVisible"
class="uw-window flex flex-col uw-clickable uw-ui-area" class="uw-window flex flex-col uw-clickable"
:class="{'fade-out': uwWindowFadeOut}" :class="{'fade-out': uwWindowFadeOut}"
@mouseenter="cancelUwWindowHide"
@mouseleave="hideUwWindow()"
> >
<PlayerUIWindow <PlayerUIWindow
:settings="settings" :settings="settings"
@ -178,59 +23,26 @@
:logger="logger" :logger="logger"
:in-player="!isGlobal" :in-player="!isGlobal"
:site="site" :site="site"
:defaultTab="defaultWindowTab"
@close="uwWindowVisible = false" @close="uwWindowVisible = false"
@preventClose="(event) => uwWindowFadeOutDisabled = event" @preventClose="(event) => uwWindowFadeOutDisabled = event"
></PlayerUIWindow> ></PlayerUIWindow>
</div> </div>
<div
v-if="triggerZoneEditorVisible"
class="context-spawn uw-ui-area"
style="z-index: 1000;"
>
<TriggerZoneEditor
:settings="settings"
:eventBus="eventBus"
:playerDimensions="playerDimensions"
>
</TriggerZoneEditor>
</div>
</template> </template>
<script> <script>
import PlayerUIWindow from '@csui/src/PlayerUIWindow.vue'; import PlayerUIWindow from './src/PlayerUIWindow.vue'
import GhettoContextMenu from '@csui/src/components/GhettoContextMenu.vue'; import BrowserDetect from '../ext/conf/BrowserDetect';
import GhettoContextMenuItem from '@csui/src/components/GhettoContextMenuItem.vue'; import Logger from '../ext/lib/Logger';
import GhettoContextMenuOption from '@csui/src/components/GhettoContextMenuOption.vue'; import Settings from '../ext/lib/Settings';
import AlignmentOptionsControlComponent from '@csui/src/PlayerUiPanels/AlignmentOptionsControlComponent.vue'; import EventBus from '../ext/lib/EventBus';
import BrowserDetect from '@src/ext/conf/BrowserDetect'; import UIProbeMixin from './src/utils/UIProbeMixin';
import Settings from '@src/ext/lib/Settings';
import EventBus from '@src/ext/lib/EventBus';
import UIProbeMixin from '@csui/src/utils/UIProbeMixin';
import KeyboardShortcutParserMixin from '@csui/src/utils/KeyboardShortcutParserMixin';
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 { export default {
components: { components: {
PlayerUIWindow, PlayerUIWindow
GhettoContextMenu,
GhettoContextMenuItem,
GhettoContextMenuOption,
AlignmentOptionsControlComponent,
SupportLevelIndicator,
TriggerZoneEditor,
ZoomControl,
}, },
mixins: [ mixins: [
UIProbeMixin, UIProbeMixin
KeyboardShortcutParserMixin,
CommsMixin
], ],
data() { data() {
return { return {
@ -249,14 +61,12 @@ export default {
uwWindowFadeOut: false, uwWindowFadeOut: false,
uwWindowCloseTimeout: undefined, uwWindowCloseTimeout: undefined,
uwWindowVisible: false, uwWindowVisible: false,
triggerZoneEditorVisible: false,
// component properties // component properties
settings: {}, settings: {},
BrowserDetect: BrowserDetect, BrowserDetect: BrowserDetect,
settingsInitialized: false, settingsInitialized: false,
eventBus: new EventBus(), eventBus: new EventBus(),
logAggregator: null,
logger: null, logger: null,
// NOTE: chromium doesn't allow us to access window.parent.location // NOTE: chromium doesn't allow us to access window.parent.location
@ -269,9 +79,6 @@ export default {
isGlobal: true, isGlobal: true,
disabled: false, disabled: false,
contextMenuActive: false,
triggerZoneActive: false,
uiVisible: true, uiVisible: true,
debugData: { debugData: {
resizer: {}, resizer: {},
@ -286,13 +93,11 @@ export default {
statusFlags: { statusFlags: {
hasDrm: undefined, hasDrm: undefined,
aardErrors: undefined,
}, },
defaultWindowTab: 'videoSettings',
saveState: {}, saveState: {},
siteSettings: undefined,
previewZoneVisible: false, selectedTab: 'videoSettings',
}; };
}, },
computed: { computed: {
@ -305,12 +110,6 @@ export default {
windowHeight() { windowHeight() {
return window.innerHeight; return window.innerHeight;
}, },
// LPT: NO ARROW FUNCTIONS IN COMPUTED,
// IS SUPER HARAM
// THINGS WILL NOT WORK IF YOU USE ARROWS
siteSupportLevel() {
return (this.site && this.siteSettings) ? this.siteSettings.data.type || 'no-support' : 'waiting';
}
}, },
watch: { watch: {
showUi(visible) { showUi(visible) {
@ -327,13 +126,16 @@ export default {
this.debugDataPrettified = JSON.stringify(this.debugData, null, 2); this.debugDataPrettified = JSON.stringify(this.debugData, null, 2);
} }
}, },
async created() { async created() {
this.logAggregator = new LogAggregator('player-overlay'); this.logger = new Logger();
this.logger = new ComponentLogger(this.logAggregator, 'PlayerOverlay.vue');
// this prolly needs to be taken out
await this.logger.init({
allowLogging: true,
});
this.settings = new Settings({afterSettingsSaved: this.updateConfig, logger: this.logger}); this.settings = new Settings({afterSettingsSaved: this.updateConfig, logger: this.logger});
this.settings.listenAfterChange(() => this.updateTriggerZones());
await this.settings.init(); await this.settings.init();
this.settingsInitialized = true; this.settingsInitialized = true;
@ -343,107 +145,60 @@ export default {
this.handleMessage(event); this.handleMessage(event);
}); });
this.eventBus.subscribeMulti( this.eventBus.subscribe('uw-config-broadcast', {function: (data) => {
if (data.type === 'drm-status') {
this.statusFlags.hasDrm = data.hasDrm;
}
}});
this.eventBus.subscribe('uw-set-ui-state', { function: (data) => {
if (data.globalUiVisible !== undefined) {
if (this.isGlobal) {
if (data.globalUiVisible) {
this.showUwWindow();
} else {
this.hideUwWindow(true);
}
// this.showPlayerUIAfterClose = data.showPlayerUIAfterClose;
} else {
// non global UIs are hidden while global overlay
// is visible and vice versa
// this.disabled = data.globalUiVisible;
this.saveState = {
uwWindowVisible: this.uwWindowVisible,
uwWindowFadeOutDisabled: this.uwWindowFadeOutDisabled,
uwWindowFadeOut: this.uwWindowFadeOut
};
this.uwWindowFadeOutDisabled = false;
this.hideUwWindow(true);
}
}
}});
this.eventBus.subscribe(
'uw-restore-ui-state',
{ {
'uw-config-broadcast': { function: (data) => {
function: if (this.saveState) {
(data) => { if (this.saveState.uwWindowVisible) {
switch (data.type) { this.showUwWindow();
case 'drm-status':
this.statusFlags.hasDrm = data.hasDrm;
break;
case 'aard-error':
this.statusFlags.aardErrors = data.aardErrors;
break;
case 'player-dimensions':
this.playerDimensionsUpdate(data.data);
break;
}
}
},
'uw-set-ui-state': {
function: (data) => {
if (data.globalUiVisible !== undefined) {
if (this.isGlobal) {
if (data.globalUiVisible) {
this.showUwWindow();
} else {
this.hideUwWindow(true);
}
// this.showPlayerUIAfterClose = data.showPlayerUIAfterClose;
} else {
// non global UIs are hidden while global overlay
// is visible and vice versa
// this.disabled = data.globalUiVisible;
this.saveState = {
uwWindowVisible: this.uwWindowVisible,
uwWindowFadeOutDisabled: this.uwWindowFadeOutDisabled,
uwWindowFadeOut: this.uwWindowFadeOut
};
this.uwWindowFadeOutDisabled = false;
this.hideUwWindow(true);
}
} }
this.uwWindowFadeOutDisabled = this.saveState.uwWindowFadeOutDisabled;
this.uwWindowFadeOut = this.saveState.uwWindowFadeOut;
} }
}, this.saveState = {};
'uw-restore-ui-state': { }
function: (data) => { }
if (this.saveState) {
if (this.saveState.uwWindowVisible) {
this.showUwWindow();
}
this.uwWindowFadeOutDisabled = this.saveState.uwWindowFadeOutDisabled;
this.uwWindowFadeOut = this.saveState.uwWindowFadeOut;
}
this.saveState = {};
}
},
'uw-restore-ui-state': {
function: (data) => {
if (this.saveState) {
if (this.saveState.uwWindowVisible) {
this.showUwWindow();
}
this.uwWindowFadeOutDisabled = this.saveState.uwWindowFadeOutDisabled;
this.uwWindowFadeOut = this.saveState.uwWindowFadeOut;
}
this.saveState = {};
}
},
'ui-trigger-zone-update': {
function: (data) => {
this.showTriggerZonePreview = data.previewZoneVisible;
// this.;
}
},
'start-trigger-zone-edit': {
function: () => {
this.triggerZoneEditorVisible = true;
this.uwWindowVisible = false;
}
},
'finish-trigger-zone-edit': {
function: () => {
this.triggerZoneEditorVisible = false;
this.showUwWindow('playerUiSettings');
}
},
},
this
); );
this.sendToParentLowLevel('uwui-get-role', null); this.sendToParentLowLevel('uwui-get-role', null);
this.sendToParentLowLevel('uwui-get-theme', null); this.sendToParentLowLevel('uwui-get-theme', null);
// console.log('player overlay created get player dims:') //
this.sendToParentLowLevel('uw-bus-tunnel', {
action: 'get-player-dimensions'
});
}, },
destroyed() {
this.eventBus.unsubscribeAll(this)
},
methods: { methods: {
/** /**
* Gets URL of the browser settings page (i think?) * Gets URL of the browser settings page (i think?)
@ -463,7 +218,6 @@ export default {
if (!this.site) { if (!this.site) {
this.origin = event.origin; this.origin = event.origin;
this.site = event.origin.split('//')[1]; this.site = event.origin.split('//')[1];
this.siteSettings = this.settings.getSiteSettings(this.site);
} }
return this.handleProbe(event.data, event.origin); // handleProbe is defined in UIProbeMixin return this.handleProbe(event.data, event.origin); // handleProbe is defined in UIProbeMixin
case 'uw-bus-tunnel': case 'uw-bus-tunnel':
@ -475,34 +229,6 @@ export default {
} }
}, },
/**
* Handles trigger zone
*/
handleTriggerZone(mouseInside) {
// this.triggerZoneActive = mouseInside;
},
acknowledgeNewFeature(featureKey) {
this.settings.active.newFeatureTracker[featureKey].show = 0;
this.settings.active.newFeatureTracker[featureKey].acknowledged = true;
this.settings.saveWithoutReload();
},
newFeatureViewUpdate(featureKey) {
if (!this.settings.active.newFeatureTracker[featureKey]) {
return;
}
try {
this.settings.active.newFeatureTracker[featureKey].show--;
this.settings.saveWithoutReload();
if (this.settings.active.newFeatureTracker[featureKey]?.show < 0) {
this.acknowledgeNewFeature(featureKey);
}
} catch (e) {
// do nothing
}
},
/** /**
* Sends message to parent _without_ using event bus. * Sends message to parent _without_ using event bus.
*/ */
@ -515,30 +241,13 @@ export default {
); );
}, },
preventContextMenuHide() { showUwWindow() {
this.contextMenuActive = true;
// refresh DRM status
this.eventBus.sendToTunnel('get-drm-status');
},
allowContextMenuHide() {
this.contextMenuActive = false;
},
setTriggerZoneActive(active, event) {
this.triggerZoneActive = active;
},
showUwWindow(tab) {
this.defaultWindowTab = tab; // can be undefined
this.uwWindowFadeOut = false; this.uwWindowFadeOut = false;
this.uwWindowVisible = true; this.uwWindowVisible = true;
this.uwTriggerZoneVisible = false; this.uwTriggerZoneVisible = false;
this.allowContextMenuHide();
// refresh DRM status // refresh DRM status
this.eventBus.sendToTunnel('get-drm-status'); this.eventBus.send('get-drm-status');
// if (this.isGlobal) { // if (this.isGlobal) {
// this.sendToParentLowLevel('uwui-clickable', undefined, {clickable: true}); // this.sendToParentLowLevel('uwui-clickable', undefined, {clickable: true});
@ -572,44 +281,29 @@ export default {
}, },
handleBusTunnelIn(payload) { handleBusTunnelIn(payload) {
this.eventBus.send( this.eventBus.send(payload.action, payload.config, payload.routingData);
payload.action,
payload.config,
{
...payload.context,
borderCrossings: {
...payload.context?.borderCrossings,
iframe: true
}
}
);
}, },
updateConfig() { selectTab(tab) {
this.settings.init(); this.selectedTab = tab;
this.$nextTick( () => this.$forceUpdate());
} }
} }
} }
</script> </script>
<style lang="scss">
.ard-blocked {
color: rgb(219, 125, 48) !important;
background-color: rgba(0,0,0,0.85) !important;
}
</style>
<style lang="scss" src="@csui/src/res-common/panels.scss" scoped module></style>
<style lang="scss" src="@csui/src/res-common/common.scss" scoped module></style>
<style lang="scss" scoped> <style lang="scss" scoped>
@import 'res/css/uwui-base.scss';
@import 'res/css/colors.scss';
@import 'res/css/font/overpass.css';
@import 'res/css/font/overpass-mono.css';
@import 'res/css/common.scss';
@import './src/res-common/_variables';
.uw-hover { .uw-hover {
position: absolute; position: absolute;
z-index: 999999999999999999;
}
.reduced-padding { z-index: 999999999999999999;
padding: 1rem !important;
} }
.uv-hover-trigger-region { .uv-hover-trigger-region {
@ -624,7 +318,7 @@ export default {
} }
.uw-window { .uw-window {
position: fixed; position: absolute;
top: 10%; top: 10%;
left: 10%; left: 10%;
@ -648,110 +342,4 @@ export default {
} }
} }
.gib-bg {
background-color: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(16px) saturate(120%);
width: fit-content;
block-size: fit-content;
}
.ui-warning {
position: absolute;
top: 0;
transform: translateY(-100%);
max-width: 16rem;
width: 16rem;
overflow: hidden;
overflow-wrap: break-word;
white-space: normal;
word-break: break-word;
word-wrap: break-word;
}
.context-spawn {
position: absolute;
top: 0;
left: 0;
width: 100dvw;
height: 100dvh;
box-sizing: border-box;
overflow: hidden;
display: flex;
flex-direction: row;
align-content: center;
align-items: center;
// width: 100%;
// height: 100%;
color: #fff;
// .context-item {
// font-size: .95rem;
// padding: 1rem 1.6rem;
// background-color: rgba(0, 0, 0, 0.5);
// backdrop-filter: blur(16px) saturate(120%);
// white-space: nowrap;
// }
// .spawn-container {
// border: 1px solid white;
// }
}
.extension-status-messages {
z-index: 1000;
text-transform: uppercase;
display: flex;
flex-direction: column;
text-align: center;
font-size: 0.9rem;
// width: 112.25%;
// transform: translate(-12.5%, 12.5%) scale(0.75);
> * {
margin-top: 0.5rem;
margin-bottom: 0.5rem;
}
}
.flex-reverse-order {
display: flex;
flex-direction: column-reverse;
}
.aard-blocked {
font-size: 0.8rem;
color: #fa6;
}
.trigger-zone-preview {
border: 4px solid #fa4;
}
.debug-1 {
border: 1px solid yellow;
&:hover {
background-color: rbba(255,255,0,0.5);
}
}
.debug-2 {
border: 1px solid blue;
&:hover {
background-color: rbba(0,0,255,.5);
}
}
</style> </style>

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="popup-panel" style="height: 100vh"> <div class="popup-panel">
<!-- <!--
NOTE the code that makes ultrawidify popup work in firefox regardless of whether the 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 extension is being displayed in a normal or a small/overflow popup breaks the popup
@ -10,43 +10,24 @@
further than that. further than that.
--> -->
<div v-if="settingsInitialized" <div v-if="settingsInitialized"
style="height: 100vh"
class="popup flex flex-col no-overflow" class="popup flex flex-col no-overflow"
:class="{'popup-chrome': ! BrowserDetect?.firefox}" :class="{'popup-chrome': ! BrowserDetect?.firefox}"
> >
<div class="flex flex-col w-full relative header" <div class="flex-row flex-nogrow flex-noshrink relative header"
> >
<div class="flex flex-row w-full" style="height: 42px"> <div class="grow shrink">
<h1 class="flex-grow"> <h1>
<span class="smallcaps">Ultrawidify</span>: <small>Quick settings</small> <span class="smallcaps">Ultrawidify</span>: <small>Quick settings</small>
</h1> </h1>
<button
class="settings-header-button"
style="align-self: stretch"
@click="showInPlayerUi()"
>
Show settings window
</button>
</div> </div>
<div v-if="BrowserDetect?.processEnvChannel !== 'stable'" class="absolute channel-info version-info">
<div class="flex flex-row w-full"> Build channel: {{BrowserDetect?.processEnvChannel}} <br/>
<div v-if="site && siteSettings" style="transform: scale(0.75) translateX(-12.5%); margin-bottom: -0.5rem; align-content: center" class="flex flex-row flex-grow items-center"> <label>Version:</label> <br/>
<div>site: {{site.host}}</div> {{ settings.getExtensionVersion() }}
<SupportLevelIndicator </div>
:siteSupportLevel="siteSupportLevel" <div v-else class="version-info">
> <label>Version:</label> <br/>
</SupportLevelIndicator> {{ settings.getExtensionVersion() }}
</div>
<!-- Version info -->
<div v-if="BrowserDetect?.processEnvChannel !== 'stable'" class="absolute channel-info version-info">
<label>Version:</label> <br/>
{{ settings.getExtensionVersion() }} (non-stable)
</div>
<div v-else class="version-info">
<label>Version:</label> <br/>
{{ settings.getExtensionVersion() }}
</div>
</div> </div>
</div> </div>
@ -60,10 +41,7 @@
v-for="tab of tabs" v-for="tab of tabs"
:key="tab.id" :key="tab.id"
class="tab flex flex-row" class="tab flex flex-row"
:class="{ :class="{'active': tab.id === selectedTab}"
'active': tab.id === selectedTab,
'highlight-tab': tab.highlight,
}"
@click="selectTab(tab.id)" @click="selectTab(tab.id)"
> >
<div class="icon-container"> <div class="icon-container">
@ -79,32 +57,34 @@
</div> </div>
<!-- CONTENT --> <!-- CONTENT -->
<div class="scrollable window-content" style="flex: 7 7; padding: 1rem;"> <div class="scrollable" style="flex: 7 7; padding: 1rem;">
<template v-if="settings && siteSettings"> <template v-if="settings && siteSettings">
<InPlayerUIAdvertisement
v-if="selectedTab === 'playerUiCtl'"
:eventBus="eventBus"
/>
<PopupVideoSettings <PopupVideoSettings
v-if="selectedTab === 'videoSettings'" v-if="selectedTab === 'videoSettings'"
:settings="settings" :settings="settings"
:eventBus="eventBus" :eventBus="eventBus"
:siteSettings="siteSettings" :siteSettings="siteSettings"
:hosts="activeHosts"
></PopupVideoSettings> ></PopupVideoSettings>
<!-- <PlayerDetectionPanel
v-if="selectedTab === 'playerDetection'"
:settings="settings"
:eventBus="eventBus"
:siteSettings="siteSettings"
:site="site.host"
>
</PlayerDetectionPanel> -->
<BaseExtensionSettings <BaseExtensionSettings
v-if="selectedTab === 'extensionSettings'" v-if="selectedTab === 'extensionSettings'"
:settings="settings" :settings="settings"
:eventBus="eventBus" :eventBus="eventBus"
:siteSettings="siteSettings" :siteSettings="siteSettings"
:site="site.host" :site="site.host"
:hosts="activeHosts"
> >
</BaseExtensionSettings> </BaseExtensionSettings>
<ChangelogPanel
v-if="selectedTab === 'changelog'"
:settings="settings"
></ChangelogPanel>
<AboutPanel
v-if="selectedTab === 'about'"
>
</AboutPanel>
</template> </template>
<template v-else>No settings or site settings found.</template> <template v-else>No settings or site settings found.</template>
</div> </div>
@ -117,30 +97,18 @@
<script> <script>
import BaseExtensionSettings from './src/PlayerUiPanels/BaseExtensionSettings.vue' import BaseExtensionSettings from './src/PlayerUiPanels/BaseExtensionSettings.vue'
import PlayerDetectionPanel from './src/PlayerUiPanels/PlayerDetectionPanel.vue' import PlayerDetectionPanel from './src/PlayerUiPanels/PlayerDetectionPanel.vue'
import ChangelogPanel from './src/PlayerUiPanels/ChangelogPanel.vue'
import PopupVideoSettings from './src/popup/panels/PopupVideoSettings.vue' import PopupVideoSettings from './src/popup/panels/PopupVideoSettings.vue'
import AboutPanel from '@csui/src/popup/panels/AboutPanel.vue' import InPlayerUIAdvertisement from './src/PlayerUiPanels/InPlayerUiAdvertisement.vue';
import Debug from '../ext/conf/Debug'; import Debug from '../ext/conf/Debug';
import BrowserDetect from '../ext/conf/BrowserDetect'; import BrowserDetect from '../ext/conf/BrowserDetect';
import Comms from '../ext/lib/comms/Comms';
import CommsClient, {CommsOrigin} from '../ext/lib/comms/CommsClient'; import CommsClient, {CommsOrigin} from '../ext/lib/comms/CommsClient';
import Settings from '../ext/lib/Settings'; import Settings from '../ext/lib/Settings';
import Logger from '../ext/lib/Logger';
import EventBus from '../ext/lib/EventBus'; import EventBus from '../ext/lib/EventBus';
import {ChromeShittinessMitigations as CSM} from '../common/js/ChromeShittinessMitigations'; 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 { export default {
components: {
Debug,
BrowserDetect,
PopupVideoSettings,
PlayerDetectionPanel,
BaseExtensionSettings,
SupportLevelIndicator,
ChangelogPanel,
AboutPanel
},
data () { data () {
return { return {
comms: undefined, comms: undefined,
@ -149,112 +117,87 @@ export default {
settingsInitialized: false, settingsInitialized: false,
narrowPopup: null, narrowPopup: null,
sideMenuVisible: null, sideMenuVisible: null,
logAggregator: undefined,
logger: undefined, logger: undefined,
site: undefined, site: undefined,
siteSettings: undefined, siteSettings: undefined,
selectedTab: 'videoSettings', selectedTab: 'playerUiCtl',
tabs: [ tabs: [
// see this for icons: https://pictogrammers.com/library/mdi/ // see this for icons: https://pictogrammers.com/library/mdi/
// {id: 'playerUiCtl', label: 'In-player UI', icon: 'artboard'}, {id: 'playerUiCtl', label: 'In-player UI', icon: 'artboard'},
{id: 'videoSettings', label: 'Video settings', icon: 'crop'}, {id: 'videoSettings', label: 'Video settings', icon: 'crop'},
// {id: 'playerDetection', label: 'Player detection', icon: 'television-play'}, // {id: 'playerDetection', label: 'Player detection', icon: 'television-play'},
{id: 'extensionSettings', label: 'Site and Extension options', icon: 'cogs' }, {id: 'extensionSettings', label: 'Site and Extension options', icon: 'cogs' },
{id: 'changelog', label: 'What\'s new', icon: 'alert-decagram' },
{id: 'about', label: 'About', icon: 'information-outline'},
], ],
} }
}, },
computed: {
siteSupportLevel() {
return (this.site && this.siteSettings) ? this.siteSettings.data.type || 'no-support' : 'waiting';
}
},
mounted() {
this.tabs.find(x => x.id === 'changelog').highlight = !this.settings.active?.whatsNewChecked;
this.requestSite();
},
async created() { async created() {
try { this.logger = new Logger();
this.logAggregator = new LogAggregator('🔵ext-popup🔵'); await this.logger.init({
this.logger = new ComponentLogger(this.logAggregator, 'Popup'); allowLogging: true,
});
this.settings = new Settings({afterSettingsSaved: () => this.updateConfig(), logAggregator: this.logAggregator}); this.settings = new Settings({afterSettingsSaved: () => this.updateConfig(), logger: this.logger});
await this.settings.init(); await this.settings.init();
this.settingsInitialized = true; this.settingsInitialized = true;
// const port = chrome.runtime.connect({name: 'popup-port'}); // const port = chrome.runtime.connect({name: 'popup-port'});
// port.onMessage.addListener( (m,p) => this.processReceivedMessage(m,p)); // port.onMessage.addListener( (m,p) => this.processReceivedMessage(m,p));
// CSM.setProperty('port', port); // CSM.setProperty('port', port);
this.eventBus = new EventBus(); this.eventBus = new EventBus();
this.eventBus.subscribe( this.eventBus.subscribe(
'set-current-site', 'set-current-site',
{ {
source: this, function: (config, context) => {
function: (config, context) => { if (this.site) {
console.log('set-current-site | this.site:', this.site, 'config.site:', config.site); if (!this.site.host) {
// dunno why this fix is needed, but sometimes it is
if (this.site) { this.site.host = config.site.host;
if (!this.site.host) {
// dunno why this fix is needed, but sometimes it is
this.site.host = config.site.host;
}
} }
this.site = config.site; }
// this.selectedSite = this.selectedSite || config.site.host; this.site = config.site;
this.siteSettings = this.settings.getSiteSettings(this.site.host); // this.selectedSite = this.selectedSite || config.site.host;
this.eventBus.setupPopupTunnelWorkaround({ this.siteSettings = this.settings.getSiteSettings(this.site.host);
origin: CommsOrigin.Popup,
comms: { this.eventBus.setupPopupTunnelWorkaround({
forwardTo: 'active' origin: CommsOrigin.Popup,
} comms: {
}); forwardTo: 'active'
}
});
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);
this.eventBus.setupPopupTunnelWorkaround({
origin: CommsOrigin.Popup,
comms: {forwardTo: 'active'}
});
// ensure we'll clean player markings on popup close
window.addEventListener("unload", () => {
CSM.port.postMessage({
cmd: 'unmark-player',
forwardToAll: true,
});
// if (BrowserDetect.anyChromium) {
// chrome.extension.getBackgroundPage().sendUnmarkPlayer({
// cmd: 'unmark-player',
// forwardToAll: true,
// });
// }
});
// get info about current site from background script
while (true) {
this.requestSite();
await this.sleep(5000);
} }
} catch (e) { );
console.error('[Popup.vue::created()] An error happened:', e)
this.comms = new CommsClient('popup-port', this.logger, this.eventBus);
this.eventBus.setComms(this.comms);
this.eventBus.setupPopupTunnelWorkaround({
origin: CommsOrigin.Popup,
comms: {forwardTo: 'active'}
});
// ensure we'll clean player markings on popup close
window.addEventListener("unload", () => {
CSM.port.postMessage({
cmd: 'unmark-player',
forwardToAll: true,
});
// if (BrowserDetect.anyChromium) {
// chrome.extension.getBackgroundPage().sendUnmarkPlayer({
// cmd: 'unmark-player',
// forwardToAll: true,
// });
// }
});
// get info about current site from background script
while (true) {
this.requestSite();
await this.sleep(5000);
} }
}, },
async updated() { async updated() {
@ -271,11 +214,12 @@ export default {
this.narrowPopup = body.offsetWidth < 600; this.narrowPopup = body.offsetWidth < 600;
} }
}, },
components: {
Debug,
BrowserDetect,
PopupVideoSettings, PlayerDetectionPanel, BaseExtensionSettings, InPlayerUIAdvertisement
},
methods: { methods: {
showInPlayerUi() {
this.eventBus.send('uw-set-ui-state', {globalUiVisible: true}, {comms: {forwardTo: 'active'}});
},
async sleep(t) { async sleep(t) {
return new Promise( (resolve,reject) => { return new Promise( (resolve,reject) => {
setTimeout(() => resolve(), t); setTimeout(() => resolve(), t);
@ -290,7 +234,6 @@ export default {
// CSM.port.postMessage({command: 'get-current-site'}); // CSM.port.postMessage({command: 'get-current-site'});
this.eventBus.send( this.eventBus.send(
'get-current-site', 'get-current-site',
{},
{ {
comms: {forwardTo: 'active'} comms: {forwardTo: 'active'}
} }
@ -305,36 +248,65 @@ export default {
selectTab(tab) { selectTab(tab) {
this.selectedTab = 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) { isDefaultFrame(frameId) {
return frameId === '__playing' || frameId === '__all'; return frameId === '__playing' || frameId === '__all';
}, },
loadHostnames() {
this.activeHosts = this.site.hostnames;
},
loadFrames() { loadFrames() {
this.activeFrames = [{ this.activeSites = [{
host: this.site.host, host: this.site.host,
isIFrame: false, // not used tho. Maybe one day isIFrame: false, // not used tho. Maybe one day
}]; }];
this.selectedSite = this.selectedSite || this.site.host;
for (const frame in this.site.frames) { // for (const frame in videoTab.frames) {
if (!this.activeFrames.find(x => x.host === this.site.frames[frame].host)) { // this.activeFrames.push({
this.activeFrames.push({ // id: `${this.site.id}-${frame}`,
id: `${this.site.id}-${frame}`, // label: videoTab.frames[frame].host,
label: this.site.frames[frame].host, // ...this.frameStore[frame],
host: this.site.frames[frame].host, // })
...this.site.frames[frame],
...this.settings.active.sites[this.site.frames[frame].host] // // 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();
}, },
getRandomColor() { getRandomColor() {
return `rgb(${Math.floor(Math.random() * 128)}, ${Math.floor(Math.random() * 128)}, ${Math.floor(Math.random() * 128)})`; return `rgb(${Math.floor(Math.random() * 128)}, ${Math.floor(Math.random() * 128)}, ${Math.floor(Math.random() * 128)})`;
},
updateConfig() {
this.settings.init();
this.$nextTick( () => this.$forceUpdate());
} }
} }
} }
@ -350,15 +322,12 @@ export default {
.header { .header {
background-color: rgb(90, 28, 13); background-color: rgb(90, 28, 13);
background-color: rgb(0,0,0);
color: #fff; color: #fff;
padding: 8px; padding: 8px;
// display: flex; display: flex;
// flex-direction: row; flex-direction: row;
// justify-content: space-between; justify-content: space-between;
border-bottom: 1px dotted #fa6;
h1 { h1 {
@ -376,21 +345,6 @@ export default {
} }
.settings-header-button {
display: flex;
flex-direction: row;
align-items: center;
padding: 0.5rem 2rem;
text-transform: lowercase;
font-variant: small-caps;
background-color: #000;
border: 1px solid #fa68;
color: #eee;
}
.site-support-info { .site-support-info {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@ -559,15 +513,6 @@ export default {
flex-shrink: 1; flex-shrink: 1;
padding: 0 !important; padding: 0 !important;
} }
&.highlight-tab {
opacity: 0.9;
color: #eee;
// .label {
// color: rgb(239, 192, 152);
// }
}
} }
} }
@ -587,10 +532,4 @@ pre {
h1 { h1 {
margin: 0; padding: 0; font-weight: 400; font-size:24px; margin: 0; padding: 0; font-weight: 400; font-size:24px;
} }
.window-content {
height: 100%;
width: 100%;
overflow: auto;
}
</style> </style>

View File

@ -1,5 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"; style="position: relative"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="color-scheme" content="light dark"> <meta name="color-scheme" content="light dark">

View File

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

View File

@ -1,12 +1,12 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en" style="position: relative"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="color-scheme" content="dark"> <meta name="color-scheme" content="dark">
<title>Ultrawidify - Content Script User Interface (in-player overlay)</title> <title>Ultrawidify - Content Script User Interface (in-player overlay)</title>
<!-- <link rel="stylesheet" href="csui.css"> --> <!-- <link rel="stylesheet" href="csui.css"> -->
</head> </head>
<body class="uw-ultrawidify-container-root" style="background-color: transparent;"> <body class="uw-ultrawidify-container-root" style="background-color: transparent">
<div id="app"></div> <div id="app"></div>
<script src="csui.js"></script> <script src="csui.js"></script>
</body> </body>

View File

@ -1,12 +1,12 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en" style="position: relative"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="color-scheme" content="light"> <meta name="color-scheme" content="light">
<title>Ultrawidify - Content Script User Interface (in-player overlay)</title> <title>Ultrawidify - Content Script User Interface (in-player overlay)</title>
<!-- <link rel="stylesheet" href="csui.css"> --> <!-- <link rel="stylesheet" href="csui.css"> -->
</head> </head>
<body class="uw-ultrawidify-container-root" style="background-color: transparent;"> <body class="uw-ultrawidify-container-root" style="background-color: transparent">
<div id="app"></div> <div id="app"></div>
<script src="csui.js"></script> <script src="csui.js"></script>
</body> </body>

View File

@ -1,11 +1,11 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en" style="position: relative"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Ultrawidify - Content Script User Interface (in-player overlay)</title> <title>Ultrawidify - Content Script User Interface (in-player overlay)</title>
<!-- <link rel="stylesheet" href="csui.css"> --> <!-- <link rel="stylesheet" href="csui.css"> -->
</head> </head>
<body class="uw-ultrawidify-container-root" style="background-color: transparent;"> <body class="uw-ultrawidify-container-root" style="background-color: transparent">
<div id="app"></div> <div id="app"></div>
<script src="csui.js"></script> <script src="csui.js"></script>
</body> </body>

View File

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

View File

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

View File

@ -1,91 +0,0 @@
<template>
<div class="flex flex-col">
<div>Ultrawidify</div>
<div class="flex flex-row">
<div v-if="notification.icon">
<mdicon :name="notification.icon"></mdicon>
</div>
<div class="flex flex-grow flex-col">
<div v-if="notification.title">{{notification.title}}</div>
<div v-if="notification.description">{{notification.description}}</div>
</div>
</div>
<div v-if="notification.actions" class="flex flex-row flex-end">
<button
v-for="(action, index) of notification.actions"
:key="index"
>
{{ action.label }}
</button>
</div>
<div>Notification countdown</div>
</div>
</template>
<script>
export default {
components: {
},
mixins: [
],
data() {
return {
notification: {}
};
},
computed: {
},
watch: {
},
async created() {
this.logger = new Logger();
window.addEventListener('message', this.handleMessage);
this.sendToParentLowLevel('init-complete', {});
},
destroyed() {
window.removeEventListener('message', this.handleMessage);
},
methods: {
/**
* Mostly intended to process messages received via window.addEventListener('message').
* This method should include minimal logic instead, it should only route messages
* to the correct function down the line.
*/
handleMessage(event) {
switch (event.data.action) {
case 'notification-data':
this.notification = event.data.payload;
}
},
/**
* Sends message to parent _without_ using event bus.
*/
sendToParentLowLevel(action, payload, lowLevelExtras = {}) {
window.parent.postMessage(
{
action, payload, ...lowLevelExtras
},
'*'
);
},
}
}
</script>
<style lang="scss">
.ard-blocked {
color: rgb(219, 125, 48) !important;
background-color: rgba(0,0,0,0.85) !important;
}
</style>
<style lang="scss" src="../../src/res-common/panels.scss" scoped module></style>
<style lang="scss" src="../../src/res-common/common.scss" scoped module></style>
<style lang="scss" scoped>
</style>

View File

@ -1,12 +0,0 @@
<!DOCTYPE html>
<html lang="en" style="position: relative">
<head>
<meta charset="UTF-8">
<title>Ultrawidify - Content Script User Interface (in-player overlay)</title>
<!-- <link rel="stylesheet" href="csui.css"> -->
</head>
<body class="uw-ultrawidify-container-root" style="background-color: transparent;">
<div id="app"></div>
<script src="csui-notification.js"></script>
</body>
</html>

View File

@ -1,11 +0,0 @@
import { createApp } from 'vue';
import Notification from './Notification';
import mdiVue from 'mdi-vue/v3';
import * as mdijs from '@mdi/js';
// NOTE — this is in-player interface for ultrawidify
// it is injected into the page in UI.init()
createApp(Notification)
.use(mdiVue, {icons: mdijs})
.mount('#app');

View File

@ -5,10 +5,6 @@
// @import "form.scss"; // @import "form.scss";
* {
box-sizing: border-box;
}
body { body {
background-color: $background-primary; background-color: $background-primary;
color: $text-normal; color: $text-normal;
@ -22,21 +18,8 @@ body {
padding: 0px; padding: 0px;
} }
.main-window::before {
content: ' ';
color: #000;
position: absolute;
left: -0.5rem;
top: -0.5rem;
z-index: -1;
width: calc(100% + 1rem);
height: calc(100% + 1rem);
background-image: url('/csui/res/img/uw-window-bg-texture-3.webp');
opacity: 0.75;
}
/* STANDARD WIDTHS AND HEIGHTS */ /* STANDARD WIDTHS AND HEIGHTS */
.w100, .w-full { .w100 {
width: 100%; width: 100%;
} }
@ -57,6 +40,8 @@ body {
padding-right: 1em; padding-right: 1em;
} }
/* overflow stuff */ /* overflow stuff */
.overflow-y-auto { .overflow-y-auto {
overflow: auto; overflow: auto;
@ -67,19 +52,6 @@ body {
scrollbar-color: rgba($primary-color, 0.5) $background-primary; scrollbar-color: rgba($primary-color, 0.5) $background-primary;
} }
::-webkit-scrollbar {
width: 5px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background-color: rgba(#fa6, 0.5);
border-radius: 20px;
border: transparent;
}
/* scrollbars for chrome/webkit */ /* scrollbars for chrome/webkit */
.overflow-y-auto::-webkit-scrollbar { .overflow-y-auto::-webkit-scrollbar {
@ -131,6 +103,7 @@ body {
} }
.label { .label {
padding-top: 1.5rem;
font-size: 1.5rem; font-size: 1.5rem;
font-variant: small-caps; font-variant: small-caps;
font-weight: 600; font-weight: 600;
@ -252,7 +225,6 @@ small {
background-color: #410 !important; background-color: #410 !important;
} }
button,
.button { .button {
/*display: inline-block;*/ /*display: inline-block;*/
// padding-top: 8px; // padding-top: 8px;
@ -266,27 +238,6 @@ button,
text-align: center; text-align: center;
cursor: pointer; cursor: pointer;
user-select: none;; 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;
}
} }
@ -367,10 +318,6 @@ button,
padding-right: 10px; padding-right: 10px;
} }
.info-button {
color: $info-color;
border: 1px solid $info-color;
}
.info { .info {
color: $info-color; color: $info-color;
padding-left: 35px; padding-left: 35px;

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 273 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

View File

@ -1,20 +1,56 @@
<template> <template>
<div <div
class="popup-panel flex flex-col uw-clickable h-full main-window relative" class="popup-panel flex flex-col uw-clickable h-full"
> >
<div class="popup-window-header"> <div class="popup-window-header">
<div class="header-title"> <div class="header-title">
<div class="popup-title">Ultrawidify <small>{{settings?.active?.version}} - {{BrowserDetect.processEnvChannel}}</small></div> <div class="popup-title">Ultrawidify <small>{{settings?.active?.version}} - {{BrowserDetect.processEnvChannel}}</small></div>
<div class="site-support-info flex flex-row"> <div class="site-support-info">
<div class="site-support-site">{{site}}</div> <div class="site-support-site">{{site}}</div>
<SupportLevelIndicator <template v-if="inPlayer">
v-if="inPlayer" <div v-if="siteSupportLevel === 'official'" class="site-support official">
:siteSupportLevel="siteSupportLevel" <mdicon name="check-decagram" />
> <div>Verified</div>
</SupportLevelIndicator> <div class="tooltip">The extension is being tested and should work on this site.</div>
</div>
<div v-if="siteSupportLevel === 'community'" class="site-support community">
<mdicon name="handshake" />
<div>Community</div>
<div class="tooltip">
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'" class="site-support no-support">
<mdicon name="help-circle-outline" />
<div>Unknown</div>
<div class="tooltip">
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'" class="site-support user-added">
<mdicon name="account" />
<div>Custom</div>
<div class="tooltip">
You have manually changed settings for this site. The extension is doing what you told it to do.
</div>
</div>
<mdicon v-if="siteSupportLevel === 'community'" class="site-support supported" name="checkbox-marked-circle" />
</template>
</div> </div>
</div> </div>
<div class="header-buttons"> <div class="header-buttons">
<div
class="header-button"
:class="{'button-active': preventClose}"
@click="setPreventClose(!preventClose)"
>
<mdicon v-if="!preventClose" name="pin-outline" :size="32" />
<mdicon v-else name="pin" :size="32" />
</div>
<div <div
class="header-button close-button" class="header-button close-button"
@click="$emit('close')" @click="$emit('close')"
@ -30,26 +66,22 @@
<div <div
v-for="tab of tabs" v-for="tab of tabs"
:key="tab.id" :key="tab.id"
class="tab"
:class="{
'active': tab.id === selectedTab,
'highlight-tab': tab.highlight,
}"
@click="selectTab(tab.id)"
> >
<div <div class="icon-container">
v-if="!tab.hidden" <mdicon
class="tab" v-if="tab.icon"
:class="{ :name="tab.icon"
'active': tab.id === selectedTab, :size="32"
'highlight-tab': tab.highlight, />
}" </div>
@click="selectTab(tab.id)" <div class="label">
> {{tab.label}}
<div class="icon-container">
<mdicon
v-if="tab.icon"
:name="tab.icon"
:size="32"
/>
</div>
<div class="label">
{{tab.label}}
</div>
</div> </div>
</div> </div>
</div> </div>
@ -73,13 +105,13 @@
<div class="flex flex-row panel-content"> <div class="flex flex-row panel-content">
<!-- Panel section --> <!-- Panel section -->
<!-- <VideoSettings <VideoSettings
v-if="selectedTab === 'videoSettings'" v-if="selectedTab === 'videoSettings'"
:settings="settings" :settings="settings"
:siteSettings="siteSettings" :siteSettings="siteSettings"
:eventBus="eventBus" :eventBus="eventBus"
:site="site" :site="site"
></VideoSettings> --> ></VideoSettings>
<PlayerDetectionPanel <PlayerDetectionPanel
v-if="selectedTab === 'playerDetection'" v-if="selectedTab === 'playerDetection'"
:siteSettings="siteSettings" :siteSettings="siteSettings"
@ -87,19 +119,11 @@
:site="site" :site="site"
> >
</PlayerDetectionPanel> </PlayerDetectionPanel>
<PlayerUiSettings
v-if="selectedTab === 'playerUiSettings'"
:settings="settings"
:siteSettings="siteSettings"
:eventBus="eventBus"
>
</PlayerUiSettings>
<BaseExtensionSettings <BaseExtensionSettings
v-if="selectedTab === 'extensionSettings'" v-if="selectedTab === 'extensionSettings'"
:settings="settings" :settings="settings"
:siteSettings="siteSettings" :siteSettings="siteSettings"
:site="site" :site="site"
:enableSettingsEditor="true"
></BaseExtensionSettings> ></BaseExtensionSettings>
<AutodetectionSettingsPanel <AutodetectionSettingsPanel
v-if="selectedTab === 'autodetectionSettings'" v-if="selectedTab === 'autodetectionSettings'"
@ -123,11 +147,6 @@
v-if="selectedTab === 'about'" v-if="selectedTab === 'about'"
> >
</AboutPanel> </AboutPanel>
<!-- <ResetBackupPanel
v-if="selectedTab === 'resetBackup'"
:settings="settings"
>
</ResetBackupPanel> -->
</div> </div>
</div> </div>
</div> </div>
@ -141,10 +160,7 @@ import PlayerDetectionPanel from './PlayerUiPanels/PlayerDetectionPanel.vue'
import VideoSettings from './PlayerUiPanels/VideoSettings.vue' import VideoSettings from './PlayerUiPanels/VideoSettings.vue'
import BrowserDetect from '../../ext/conf/BrowserDetect' import BrowserDetect from '../../ext/conf/BrowserDetect'
import ChangelogPanel from './PlayerUiPanels/ChangelogPanel.vue' import ChangelogPanel from './PlayerUiPanels/ChangelogPanel.vue'
import AboutPanel from '@csui/src/PlayerUiPanels/AboutPanel.vue' import AboutPanel from './PlayerUiPanels/AboutPanel.vue'
import PlayerUiSettings from './PlayerUiPanels/PlayerUiSettings.vue'
import ResetBackupPanel from './PlayerUiPanels/ResetBackupPanel.vue'
import SupportLevelIndicator from '@csui/src/components/SupportLevelIndicator.vue'
export default { export default {
components: { components: {
@ -153,11 +169,8 @@ export default {
BaseExtensionSettings, BaseExtensionSettings,
AutodetectionSettingsPanel, AutodetectionSettingsPanel,
DebugPanel, DebugPanel,
PlayerUiSettings,
ChangelogPanel, ChangelogPanel,
AboutPanel, AboutPanel
SupportLevelIndicator,
ResetBackupPanel,
}, },
mixins: [], mixins: [],
data() { data() {
@ -167,17 +180,16 @@ export default {
}, },
tabs: [ tabs: [
// {id: 'videoSettings', label: 'Video settings', icon: 'crop'}, {id: 'videoSettings', label: 'Video settings', icon: 'crop'},
{id: 'extensionSettings', label: 'Site and Extension options', icon: 'cogs' },
{id: 'playerUiSettings', label: 'UI and keyboard', icon: 'movie-cog-outline' },
{id: 'playerDetection', label: 'Player detection', icon: 'television-play'}, {id: 'playerDetection', label: 'Player detection', icon: 'television-play'},
{id: 'autodetectionSettings', label: 'Autodetection options', icon: 'auto-fix'}, {id: 'extensionSettings', label: 'Site and Extension options', icon: 'cogs' },
// {id: 'autodetectionSettings', label: 'Autodetection options', icon: ''},
// {id: 'advancedOptions', label: 'Advanced options', icon: 'cogs' }, // {id: 'advancedOptions', label: 'Advanced options', icon: 'cogs' },
{id: 'changelog', label: 'What\'s new', icon: 'alert-decagram' }, // {id: 'debugging', label: 'Debugging', icon: 'bug-outline' }
{id: 'about', label: 'About', icon: 'information-outline'}, {id: 'changelog', label: 'What\'s new', icon: 'information-box-outline' },
{id: 'debugging', label: 'Debugging', icon: 'bug-outline', hidden: true}, {id: 'about', label: 'About', icon: 'star-four-points-circle'}
], ],
selectedTab: 'extensionSettings', selectedTab: 'videoSettings',
BrowserDetect: BrowserDetect, BrowserDetect: BrowserDetect,
preventClose: false, preventClose: false,
siteSettings: null, siteSettings: null,
@ -188,8 +200,7 @@ export default {
'eventBus', 'eventBus',
'logger', 'logger',
'in-player', 'in-player',
'site', 'site'
'defaultTab'
], ],
computed: { computed: {
// LPT: NO ARROW FUNCTIONS IN COMPUTED, // LPT: NO ARROW FUNCTIONS IN COMPUTED,
@ -200,30 +211,17 @@ export default {
} }
}, },
created() { created() {
this.settings.listenAfterChange(this.setDebugTabVisibility);
if (this.defaultTab) {
this.selectedTab = this.defaultTab;
}
this.siteSettings = this.settings.getSiteSettings(this.site); 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( this.eventBus.subscribe(
'uw-show-ui', 'uw-show-ui',
{ () => {
source: this, if (this.inPlayer) {
function: () => { return; // show-ui is only intended for global overlay
if (this.inPlayer) { }
return; // show-ui is only intended for global overlay
}
},
} }
) )
this.setDebugTabVisibility();
},
destroyed() {
this.settings.removeListenerAfterChange(this.setDebugTabVisibility);
this.eventBus.unsubscribeAll(this);
}, },
methods: { methods: {
/** /**
@ -238,12 +236,6 @@ export default {
setPreventClose(bool) { setPreventClose(bool) {
this.preventClose = bool; this.preventClose = bool;
this.$emit('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;
}
} }
} }
} }
@ -262,8 +254,6 @@ export default {
// height: 100%; // height: 100%;
// } // }
.tab-row { .tab-row {
width: 22rem; width: 22rem;
flex-grow: 0; flex-grow: 0;
@ -276,6 +266,86 @@ export default {
overflow: hidden; overflow: hidden;
} }
.site-support-info {
display: flex;
flex-direction: row;
align-items: center;
.site-support-site {
font-size: 1.5em;
}
.site-support {
display: inline-flex;
flex-direction: row;
align-items: center;
margin-left: 1rem;
border-radius: 8px;
padding: 0rem 1.5rem 0rem 1rem;
position: relative;
.tooltip {
padding: 1rem;
display: none;
position: absolute;
bottom: 0;
transform: translateY(110%);
width: 42em;
background-color: rgba(0,0,0,0.90);
color: #ccc;
}
&:hover {
.tooltip {
display: block;
}
}
.mdi {
margin-right: 1rem;
}
&.official {
background-color: #fa6;
color: #000;
.mdi {
fill: #000 !important;
}
}
&.community {
background-color: rgb(85, 85, 179);
color: #fff;
.mdi {
fill: #fff !important;
}
}
&.no-support {
background-color: rgb(138, 65, 126);
color: #eee;
.mdi {
fill: #eee !important;
}
}
&.user-added {
border: 1px solid #ff0;
color: #ff0;
.mdi {
fill: #ff0 !important;
}
}
}
}
.content { .content {
flex-grow: 1; flex-grow: 1;
@ -316,16 +386,6 @@ export default {
} }
.site-support-info {
display: flex;
flex-direction: row;
align-items: bottom;
.site-support-site {
font-size: 1.5em;
}
}
.popup-panel { .popup-panel {
background-color: rgba(0,0,0,0.50); background-color: rgba(0,0,0,0.50);
color: #fff; color: #fff;
@ -385,7 +445,7 @@ export default {
padding: 2rem; padding: 2rem;
font-size: 1.5rem; font-size: 1.5rem;
height: 6rem; height: 4rem;
border-bottom: 1px solid rgba(128, 128, 128, 0.5); border-bottom: 1px solid rgba(128, 128, 128, 0.5);
border-top: 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%"> <div class="w-[1/2]" style="width: 50%">
<h2>Report a problem</h2> <h2>Report a problem</h2>
<p> <p>
Please report <strike>undocumented features</strike> bugs using one of the following options (in order of preference): You may report <strike>undocumented features</strike> bugs using one of the following options (in order of preference):
</p> </p>
<ul> <ul>
<li> <a target="_blank" href="https://github.com/tamius-han/ultrawidify/issues"><b>Github (preferred)</b></a><br/></li> <li> <a target="_blank" href="https://github.com/tamius-han/ultrawidify/issues"><b>Github (preferred)</b></a><br/></li>
@ -29,7 +29,7 @@
</ul> </ul>
<p> <p>
<small> <small>
[1] If using anything other than Firefox, Chrome, or Edge, please check if issue also happens in Chrome or Edge. Bugs affecting Opera will only get fixed if they also affect Chrome, because <a target="_blank" href="https://stuff.tamius.net/sacred-texts/2024/06/08/why-im-boycotting-opera/">I'm salty</a>.<br/> [1] If using anything other than Firefox, Chrome, or Edge, please check if issue also happens in Chrome or Edge. Bugs affecting Opera will only get fixed if they also affect Chrome, because <a href="https://stuff.tamius.net/sacred-texts/2024/06/08/why-im-boycotting-opera/">I'm salty</a>.<br/>
[2] It is recommended that users of Microsoft Edge install Ultrawidify from the Microsoft Edge Addons source [2] It is recommended that users of Microsoft Edge install Ultrawidify from the Microsoft Edge Addons source
</small> </small>
</p> </p>
@ -41,10 +41,28 @@
If you think I deserve money for the work I did up to this point, you can bankroll my caffeine addiction. If you think I deserve money for the work I did up to this point, you can bankroll my caffeine addiction.
</p> </p>
<p class="text-center"> <p class="text-center">
<a class="donate" href="https://www.paypal.com/paypalme/tamius" target="_blank">Donate on Paypal</a> <a class="donate" href="https://www.paypal.com/paypalme/tamius">Donate on Paypal</a>
</p>
<h2>Fun stuff</h2>
<p>
This is probably a bad idea but
</p> </p>
<p> <p>
I also have <a href="https://instagram.com/shaman_urkog" target="_blank">instagram with nerdy shit</a> <small>(mini painting + various fantasy events)</small>. Are you attending Isle of Wonders on Cres, Croatia, between 28. 6. and 30. 6.? So am I, by official duty.
</p>
<p>
Club Amulet D20 is forecasted to have a stand there, and I am forecasted to be in the general vicinity of it (barring any unexpected circumstances). I'll be either taking photos, painting minis, or doing heatstroke any% in rather rudamentary costume.
</p>
<p>
If you're there, you can swing around to say 'hi' or provide some validation, or paint some minis. Rumor has it Conquest will have paint&take event.
</p>
<p>
Tamius
</p>
<p>
</p>
<p>
<small>I am not paid to shill this.</small>
</p> </p>
</div> </div>
</div> </div>

View File

@ -1,58 +1,64 @@
<template> <template>
<div class="alignment-box" :class="{large: large}"> <div class="alignment-box">
<div <div class="row">
class="col top left" <div
@click="align(VideoAlignment.Left, VideoAlignment.Top)" class="col top left"
> @click="align(VideoAlignment.Left, VideoAlignment.Top)"
<div class="alignment-ui"></div> >
<div class="alignment-ui"></div>
</div>
<div
class="col top center"
@click="align(VideoAlignment.Center, VideoAlignment.Top)"
>
<div class="alignment-ui"></div>
</div>
<div
class="col top right"
@click="align(VideoAlignment.Right, VideoAlignment.Top)"
>
<div class="alignment-ui"></div>
</div>
</div> </div>
<div <div class="row">
class="col top center" <div
@click="align(VideoAlignment.Center, VideoAlignment.Top)" class="col ycenter left"
> @click="align(VideoAlignment.Left, VideoAlignment.Center)"
<div class="alignment-ui"></div> >
<div class="alignment-ui"></div>
</div>
<div
class="col ycenter center"
@click="align(VideoAlignment.Center, VideoAlignment.Center)"
>
<div class="alignment-ui"></div>
</div>
<div
class="col ycenter right"
@click="align(VideoAlignment.Right, VideoAlignment.Center)"
>
<div class="alignment-ui"></div>
</div>
</div> </div>
<div <div class="row">
class="col top right" <div
@click="align(VideoAlignment.Right, VideoAlignment.Top)" class="col bottom left"
> @click="align(VideoAlignment.Left, VideoAlignment.Bottom)"
<div class="alignment-ui"></div> >
</div> <div class="alignment-ui"></div>
<div </div>
class="col ycenter left" <div
@click="align(VideoAlignment.Left, VideoAlignment.Center)" class="col bottom center"
> @click="align(VideoAlignment.Center, VideoAlignment.Bottom)"
<div class="alignment-ui"></div> >
</div> <div class="alignment-ui"></div>
<div </div>
class="col ycenter center" <div
@click="align(VideoAlignment.Center, VideoAlignment.Center)" class="col bottom right"
> @click="align(VideoAlignment.Right, VideoAlignment.Bottom)"
<div class="alignment-ui"></div> >
</div> <div class="alignment-ui"></div>
<div </div>
class="col ycenter right"
@click="align(VideoAlignment.Right, VideoAlignment.Center)"
>
<div class="alignment-ui"></div>
</div>
<div
class="col bottom left"
@click="align(VideoAlignment.Left, VideoAlignment.Bottom)"
>
<div class="alignment-ui"></div>
</div>
<div
class="col bottom center"
@click="align(VideoAlignment.Center, VideoAlignment.Bottom)"
>
<div class="alignment-ui"></div>
</div>
<div
class="col bottom right"
@click="align(VideoAlignment.Right, VideoAlignment.Bottom)"
>
<div class="alignment-ui"></div>
</div> </div>
</div> </div>
</template> </template>
@ -60,10 +66,10 @@
<script> <script>
import VideoAlignmentType from '../../../common/enums/VideoAlignmentType.enum'; import VideoAlignmentType from '../../../common/enums/VideoAlignmentType.enum';
export default { export default {
props: [ props: [
'eventBus', 'eventBus'
'large',
], ],
data() { data() {
return { return {
@ -81,24 +87,23 @@ export default {
<style lang="scss" scoped module> <style lang="scss" scoped module>
.alignment-box { .alignment-box {
aspect-ratio: 1; aspect-ratio: 1;
// width: 100%; width: 100%;
// min-width: 40px; min-width: 40px;
// max-width: 80px; max-width: 80px;
display: grid; display: flex;
grid-template-columns: repeat(3, 1fr); flex-direction: column;
gap: 0.5rem;
&.large { .row {
max-width: 15rem; flex: 0 0 33%;
.col { display: flex;
width: 4rem; flex-direction: row;
height: 4rem;
}
} }
.col { .col {
flex: 0 0 33%;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@ -106,6 +111,7 @@ export default {
background-color: rgba(0,0,0,0.25); background-color: rgba(0,0,0,0.25);
cursor: pointer; cursor: pointer;
margin: 0.125rem;
padding: 0.5rem; padding: 0.5rem;
width: 1.5rem; width: 1.5rem;
height: 1.5rem; height: 1.5rem;

View File

@ -3,253 +3,222 @@
<div class="flex flex-row flex-wrap"> <div class="flex flex-row flex-wrap">
<!-- AARD performance metrics --> <!-- AARD performance metrics -->
<div> <div class="sub-panel">
<div class="flex flex-row"> <div class="flex flex-row">
<h1>Automatic Aspect Ratio Detection</h1> <h1><mdicon name="television-play" :size="32" /> Automatic Aspect Ratio Detection</h1>
</div> </div>
<div class="aard-settings-group"> <div class="sub-panel-content">
<p>
<b>Autodetection performance</b>
</p>
<p>
Automatic aspect ratio detection is a resource-hungry feature.
This page allows you to trade autodetection accuracy and/or frequency for
better performance.
</p>
<p>
Note that some browsers <a href="https://developer.mozilla.org/en-US/docs/Web/API/Performance/now" target="_blank">limit the accuracy of time measurements</a>, though once the bars go past the blue line those limitations are largely inconsequential.
</p>
<div class="performance-graph-container">
<div class="performance-graph">
<div class="time-budget hz144"></div>
<div class="time-budget hz120"></div>
<div class="time-budget hz60"></div>
<div class="time-budget hz30"></div>
<div class="time-budget hz24"></div>
<div class="time-budget rest"></div>
<!-- the last time i tried to comment out this block, it didn't work properly v-if="false" it is --> <div class="bar-container">
<div v-if="false"> <div class="average-case">
<p> <div class="stats">
<b>Autodetection performance</b> <b>Average: </b>
</p> <span class="draw">draw (main) {{(performanceData?.imageDraw?.averageTime ?? 0).toFixed(1)}} ms</span> |
<p> <span class="draw-blackframe">blackframe {{(performanceData?.blackFrame?.averageTime ?? 0).toFixed(1)}} ms</span> |
Automatic aspect ratio detection is a resource-hungry feature. <span class="processing">
This page allows you to trade autodetection accuracy and/or frequency for processing {{
better performance. Math.max(
</p> (performanceData?.total?.averageTime ?? 0)
<p> - (performanceData?.imageDraw?.averageTime ?? 0)
Note that some browsers <a href="https://developer.mozilla.org/en-US/docs/Web/API/Performance/now" target="_blank">limit the accuracy of time measurements</a>, though once the bars go past the blue line those limitations are largely inconsequential. - (performanceData?.blackFrame?.averageTime ?? 0),
</p> 0
<div class="performance-graph-container">
<div class="performance-graph">
<div class="time-budget hz144"></div>
<div class="time-budget hz120"></div>
<div class="time-budget hz60"></div>
<div class="time-budget hz30"></div>
<div class="time-budget hz24"></div>
<div class="time-budget rest"></div>
<div class="bar-container">
<div class="average-case">
<div class="stats">
<b>Average: </b>
<span class="draw">draw (main) {{(performanceData?.imageDraw?.averageTime ?? 0).toFixed(1)}} ms</span> |
<span class="draw-blackframe">blackframe {{(performanceData?.blackFrame?.averageTime ?? 0).toFixed(1)}} ms</span> |
<span class="processing">
processing {{
Math.max(
(performanceData?.total?.averageTime ?? 0)
- (performanceData?.imageDraw?.averageTime ?? 0)
- (performanceData?.blackFrame?.averageTime ?? 0),
0
).toFixed(1)
}} ms
</span>
</div>
<div class="bar">
<div
class="draw"
:style="{'width': (performanceData?.imageDraw?.averageTime ?? 0) + '%'}"
>
</div>
<div
class="draw-blackframe"
:style="{'width': (performanceData?.blackFrame?.averageTime ?? 0) + '%'}"
>
</div>
<div
class="processing"
:style="{
'width': Math.max(
(performanceData?.total?.averageTime ?? 0)
- (performanceData?.imageDraw?.averageTime ?? 0)
- (performanceData?.blackFrame?.averageTime ?? 0),
0
) + '%'
}"
>
</div>
</div>
</div>
<div class="worst-case">
<div class="stats">
<b>Worst: </b>
<span class="draw">draw (main) {{(performanceData?.imageDraw?.worstTime ?? 0).toFixed(1)}} ms</span> |
<span class="draw-blackframe">blackframe {{(performanceData?.blackFrame?.worstTime ?? 0).toFixed(1)}} ms</span> |
<span class="processing">
processing {{
Math.max(
(performanceData?.total?.worstTime ?? 0)
- (performanceData?.imageDraw?.worstTime ?? 0)
- (performanceData?.blackFrame?.worstTime ?? 0),
0
).toFixed(1)
}} ms
</span>
</div>
<div class="bar">
<div
class="draw"
:style="{'width': (performanceData?.imageDraw?.worstTime ?? 0) + '%'}"
>
</div>
<div
class="draw-blackframe"
:style="{'width': (performanceData?.blackFrame?.worstTime ?? 0) + '%'}"
>
</div>
<div
class="processing"
:style="{
'width': Math.max(
(performanceData?.total?.worstTime ?? 0)
- (performanceData?.imageDraw?.worstTime ?? 0)
- (performanceData?.blackFrame?.worstTime ?? 0),
0
) + '%'
}"
>
</div>
</div>
</div>
<div class="average-case">
<div class="stats">
<b>AR change (average): </b>
<span class="draw">draw (main) {{(performanceData?.imageDraw?.averageTime ?? 0).toFixed(1)}} ms</span> |
<span class="draw-blackframe">blackframe {{(performanceData?.blackFrame?.averageTime ?? 0).toFixed(1)}} ms</span> |
<span class="processing">processing {{
(
(performanceData?.fastLetterbox?.averageTime ?? 0)
+ (performanceData?.edgeDetect?.averageTime ?? 0)
).toFixed(1) ).toFixed(1)
}} ms</span> }} ms
</span>
</div>
<div class="bar">
<div
class="draw"
:style="{'width': (performanceData?.imageDraw?.averageTime ?? 0) + '%'}"
>
</div> </div>
<div class="bar"> <div
<div class="draw-blackframe"
class="draw" :style="{'width': (performanceData?.blackFrame?.averageTime ?? 0) + '%'}"
:style="{'width': (performanceData?.imageDraw?.averageTime ?? 0) + '%'}" >
> </div>
</div> <div
<div class="processing"
class="draw-blackframe" :style="{
:style="{'width': (performanceData?.blackFrame?.averageTime ?? 0) + '%'}" 'width': Math.max(
> (performanceData?.total?.averageTime ?? 0)
</div> - (performanceData?.imageDraw?.averageTime ?? 0)
<div - (performanceData?.blackFrame?.averageTime ?? 0),
class="processing" 0
:style="{ ) + '%'
'width': ( }"
(performanceData?.fastLetterbox?.averageTime ?? 0) >
+ (performanceData?.edgeDetect?.averageTime ?? 0)
) + '%'
}"
>
</div>
</div> </div>
</div> </div>
<div class="worst-case"> </div>
<div class="stats"> <div class="worst-case">
<b>AR change (worst): </b> <div class="stats">
<span class="draw">draw (main) {{(performanceData?.imageDraw?.worstTime ?? 0).toFixed(1)}} ms</span> | <b>Worst: </b>
<span class="draw-blackframe">blackframe {{(performanceData?.blackFrame?.worstTime ?? 0).toFixed(1)}} ms</span> | <span class="draw">draw (main) {{(performanceData?.imageDraw?.worstTime ?? 0).toFixed(1)}} ms</span> |
<span class="processing">processing {{ <span class="draw-blackframe">blackframe {{(performanceData?.blackFrame?.worstTime ?? 0).toFixed(1)}} ms</span> |
( <span class="processing">
(performanceData?.fastLetterbox?.worstTime ?? 0) processing {{
+ (performanceData?.edgeDetect?.worstTime ?? 0) Math.max(
(performanceData?.total?.worstTime ?? 0)
- (performanceData?.imageDraw?.worstTime ?? 0)
- (performanceData?.blackFrame?.worstTime ?? 0),
0
).toFixed(1) ).toFixed(1)
}} ms</span> }} ms
</span>
</div>
<div class="bar">
<div
class="draw"
:style="{'width': (performanceData?.imageDraw?.worstTime ?? 0) + '%'}"
>
</div> </div>
<div class="bar"> <div
<div class="draw-blackframe"
class="draw" :style="{'width': (performanceData?.blackFrame?.worstTime ?? 0) + '%'}"
:style="{'width': (performanceData?.imageDraw?.worstTime ?? 0) + '%'}" >
> </div>
</div> <div
<div class="processing"
class="draw-blackframe" :style="{
:style="{'width': (performanceData?.blackFrame?.worstTime ?? 0) + '%'}" 'width': Math.max(
> (performanceData?.total?.worstTime ?? 0)
</div> - (performanceData?.imageDraw?.worstTime ?? 0)
<div - (performanceData?.blackFrame?.worstTime ?? 0),
class="processing" 0
:style="{ ) + '%'
'width': ( }"
(performanceData?.fastLetterbox?.worstTime ?? 0) >
+ (performanceData?.edgeDetect?.worstTime ?? 0)
) + '%'
}"
>
</div>
</div> </div>
</div> </div>
</div> </div>
<div class="average-case">
<div class="stats">
<b>AR change (average): </b>
<span class="draw">draw (main) {{(performanceData?.imageDraw?.averageTime ?? 0).toFixed(1)}} ms</span> |
<span class="draw-blackframe">blackframe {{(performanceData?.blackFrame?.averageTime ?? 0).toFixed(1)}} ms</span> |
<span class="processing">processing {{
(
(performanceData?.fastLetterbox?.averageTime ?? 0)
+ (performanceData?.edgeDetect?.averageTime ?? 0)
).toFixed(1)
}} ms</span>
</div>
<div class="bar">
<div
class="draw"
:style="{'width': (performanceData?.imageDraw?.averageTime ?? 0) + '%'}"
>
</div>
<div
class="draw-blackframe"
:style="{'width': (performanceData?.blackFrame?.averageTime ?? 0) + '%'}"
>
</div>
<div
class="processing"
:style="{
'width': (
(performanceData?.fastLetterbox?.averageTime ?? 0)
+ (performanceData?.edgeDetect?.averageTime ?? 0)
) + '%'
}"
>
</div>
</div>
</div>
<div class="worst-case">
<div class="stats">
<b>AR change (worst): </b>
<span class="draw">draw (main) {{(performanceData?.imageDraw?.worstTime ?? 0).toFixed(1)}} ms</span> |
<span class="draw-blackframe">blackframe {{(performanceData?.blackFrame?.worstTime ?? 0).toFixed(1)}} ms</span> |
<span class="processing">processing {{
(
(performanceData?.fastLetterbox?.worstTime ?? 0)
+ (performanceData?.edgeDetect?.worstTime ?? 0)
).toFixed(1)
}} ms</span>
</div>
<div class="bar">
<div
class="draw"
:style="{'width': (performanceData?.imageDraw?.worstTime ?? 0) + '%'}"
>
</div>
<div
class="draw-blackframe"
:style="{'width': (performanceData?.blackFrame?.worstTime ?? 0) + '%'}"
>
</div>
<div
class="processing"
:style="{
'width': (
(performanceData?.fastLetterbox?.worstTime ?? 0)
+ (performanceData?.edgeDetect?.worstTime ?? 0)
) + '%'
}"
>
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
<div class="settings-segment"> <div class="settings-segment">
<!-- <h2>Basic settings</h2> --> <h2>Basic settings</h2>
<!-- <div class="field"> <div class="option">
<div class="label"> <div class="name">
Stop autodetection after first detection: Autodetection frequency
</div> </div>
<div class=""> <div class="description">
<input type="checkbox" v-model="settings.active.arDetect.earlyStopOptions.stopAfterFirstDetection" /> Shorter intervals (left side of the slider) are more responsive to changes in aspect ratio detections,
but requires more system resources.
</div> </div>
</div> <div class="indent">
<div class="field"> <div class="flex flex-row row-padding">
<div class="label"> <div class="flex flex-input">
Stop detection after a period of time: More often&nbsp;<small>(~60/s)</small>
</div> <input type="range"
<div class=""> :value="Math.log(settings.active.arDetect.timers.playing)"
<input type="checkbox" v-model="settings.active.arDetect.earlyStopOptions.stopAfterTimeout" /> @change="setArCheckFrequency($event.target.value)"
</div> min="2.3"
</div> max="9.3"
<div class="field"> step="any"
<div class="label"> />
Stop detection after: &nbsp; Less often&nbsp;<small>(~1/10s)</small>
</div> </div>
<div class="input"> </div>
<input type="input" v-model="settings.active.arDetect.earlyStopOptions.stopTimeout" />
<div class="unit">seconds</div>
</div>
</div> -->
<div class="field">
<div class="label">Autodetection frequency (time between samples)</div>
<div class="range-input">
<input
type="range"
:value="Math.log(settings.active.arDetect.timers.playing)"
@change="setArCheckFrequency($event.target.value)"
min="2.3"
max="9.3"
step="0.01"
/>
<input
v-model="settings.active.arDetect.timers.playing"
@change="setArCheckFrequency($event.target.value)"
class="input"
type="text"
>
<div class="unit">ms</div>
</div> </div>
</div> </div>
<div class="field"> <div class="option">
<div class="label">Frame extraction canvas type:</div> <div class="name">
<div class="select"> Autodetection sensitivity
<select v-model="settings.active.arDetect.aardType" @change="settings.saveWithoutReload"> </div>
<option value="auto">Automatic</option> <div class="description">
<option value="webgl">WebGL only</option>
<option value="legacy">Legacy / fallback</option>
</select>
</div> </div>
</div> </div>
@ -258,41 +227,30 @@
<div class="input"> <div class="input">
<input v-model="settings.active.arDetect.allowedMisaligned" /> <input v-model="settings.active.arDetect.allowedMisaligned" />
</div> </div>
<div class="hint"> <div class="description">
Ultrawidify detects letterbox only if video is vertically centered. Some people are bad at vertically Ultrawidify detects letterbox only if video is vertically centered. Some people are bad at vertically
centering the content, though. This is how off-center the video can be before autodetection will centering the content, though. This is how off-center the video can be before autodetection will
refuse to crop it (% of total height). refuse to crop it.
</div> </div>
</div> </div>
<div class="option">
<div class="name">Video sample size</div>
<div class="input">
<input v-model="settings.active.arDetect.canvasDimensions.sampleCanvas.width" /> x <input v-model="settings.active.arDetect.canvasDimensions.sampleCanvas.height" />
</div>
</div>
<div class="field">
<div class="label">Sample columns:</div>
<div class="input"><input v-model="settings.active.arDetect.sampling.staticCols" /></div>
</div>
<div class="field">
<div class="label">Sample rows:</div>
<div class="input"><input v-model="settings.active.arDetect.sampling.staticRows" /></div>
</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> </div>
</div> </div>
@ -309,15 +267,14 @@ import AspectRatioType from '../../../common/enums/AspectRatioType.enum';
import StretchType from '../../../common/enums/StretchType.enum'; import StretchType from '../../../common/enums/StretchType.enum';
import CropModePersistence from '../../../common/enums/CropModePersistence.enum'; import CropModePersistence from '../../../common/enums/CropModePersistence.enum';
import AlignmentOptionsControlComponent from './AlignmentOptionsControlComponent.vue'; import AlignmentOptionsControlComponent from './AlignmentOptionsControlComponent.vue';
import JsonEditor from '@csui/src/components/JsonEditor';
export default { export default {
components: { data() {
ShortcutButton, return {
EditShortcutButton, exec: null,
Button, performanceData: {},
AlignmentOptionsControlComponent, graphRefreshInterval: undefined,
JsonEditor }
}, },
mixins: [ mixins: [
], ],
@ -327,41 +284,28 @@ export default {
'eventBus', 'eventBus',
'site' 'site'
], ],
data() {
return {
exec: null,
performanceData: {},
graphRefreshInterval: undefined,
settingsJson: {},
}
},
computed: {
},
created() { created() {
this.eventBus.subscribe( this.eventBus.subscribe('uw-config-broadcast', {function: (config) => this.handleConfigBroadcast(config)});
'uw-config-broadcast',
{
source: this,
function: (config) => this.handleConfigBroadcast(config)
}
);
}, },
mounted() { mounted() {
this.eventBus.sendToTunnel('get-aard-timing'); this.eventBus.sendToTunnel('get-aard-timing');
this.graphRefreshInterval = setInterval(() => this.eventBus.sendToTunnel('get-aard-timing'), 500); this.graphRefreshInterval = setInterval(() => this.eventBus.sendToTunnel('get-aard-timing'), 500);
this.resetSettingsEditor();
}, },
destroyed() { destroyed() {
this.eventBus.unsubscribeAll(this);
clearInterval(this.graphRefreshInterval); clearInterval(this.graphRefreshInterval);
}, },
components: {
ShortcutButton,
EditShortcutButton,
Button,
AlignmentOptionsControlComponent
},
computed: {
},
methods: { methods: {
async openOptionsPage() { async openOptionsPage() {
BrowserDetect.runtime.openOptionsPage(); BrowserDetect.runtime.openOptionsPage();
}, },
setArCheckFrequency(event) {
this.settings.active.arDetect.timers.playing = Math.floor(Math.pow(Math.E, event));
},
refreshGraph() { refreshGraph() {
this.eventBus.sendToTunnel('get-aard-timing'); this.eventBus.sendToTunnel('get-aard-timing');
}, },
@ -371,29 +315,17 @@ export default {
this.$nextTick( () => this.$forceUpdate() ); 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> </script>
<style lang="scss" src="../../res/css/flex.scss" scoped module></style> <style lang="scss" src="../../res/css/flex.scss" scoped module></style>
<style lang="scss" src="@csui/src/res-common/panels.scss" scoped module></style> <style lang="scss" src="../res-common/panels.scss" scoped module></style>
<style lang="scss" src="@csui/src/res-common/common.scss" scoped module></style> <style lang="scss" src="../res-common/common.scss" scoped module></style>
<style lang="scss" scoped module> <style lang="scss" scoped module>
@import '../res-common/variables'; @import '../res-common/variables';
// .aard-settings-group {
// max-width: 69rem;
// }
.performance-graph-container { .performance-graph-container {
position: relative; position: relative;

View File

@ -1,195 +1,85 @@
<template> <template>
<div class="flex flex-row w-full h-full"> <div class="flex flex-col w-100">
<div class="flex flex-col w-full">
<!-- TAB ROW --> <!-- TAB ROW -->
<div class="flex flex-row"> <div class="flex flex-row">
<div <div
class="tab" class="tab"
:class="{'active': tab === 'siteSettings'}" :class="{'active': tab === 'siteSettings'}"
@click="setTab('siteSettings')" @click="setTab('siteSettings')"
> >
Current site<br/> Settings for current site<br/>
<small>{{ site }}</small> <small>{{ site }}</small>
</div>
<div
v-if="hosts"
class="tab"
:class="{'active': tab === 'embeddedSites'}"
@click="setTab(tab = 'embeddedSites')"
>
Embedded content ({{hosts?.length}} {{hosts?.length === 1 ? 'site' : 'sites'}})
</div>
<div
class="tab"
:class="{'active': tab === 'otherSites'}"
@click="setTab(tab = 'otherSites')"
>
Defaults & other sites
</div>
</div> </div>
<div
<template v-if="tab === 'siteSettings' && siteSettings"> class="tab"
<SiteExtensionSettings :class="{'active': tab === 'extensionSettings'}"
v-if="settings" @click="setTab(tab = 'extensionSettings')"
:settings="settings" >
:siteSettings="siteSettings" Default settings for extension
:isDefaultConfiguration="false" </div>
></SiteExtensionSettings> <div
</template> class="tab"
:class="{'active': tab === 'otherSites'}"
<template v-if="hosts && tab === 'embeddedSites' && globalSettings"> @click="setTab(tab = 'otherSites')"
<FrameSiteSettings >
v-if="settings" Settings for other sites
:hosts="hosts"
:settings="settings"
></FrameSiteSettings>
</template>
<template v-if="tab === 'otherSites'">
<OtherSiteSettings
v-if="settings"
:settings="settings"
>
</OtherSiteSettings>
</template>
<!-- Reset options -->
<div class="flex flex-col" style="margin-top: 2rem">
<h2>Reset and backup</h2>
<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
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> </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> <template v-if="tab === 'siteSettings' && siteSettings">
<div> <!-- <div class="button">
<JsonEditor Reset settings for site
v-model="settingsJson" </div> -->
> <SiteExtensionSettings
</JsonEditor> v-if="settings"
</div> :settings="settings"
:siteSettings="siteSettings"
:isDefaultConfiguration="false"
></SiteExtensionSettings>
</template>
<template v-if="tab === 'extensionSettings' && globalSettings">
<SiteExtensionSettings
v-if="settings"
:settings="settings"
:siteSettings="globalSettings"
:isDefaultConfiguration="true"
></SiteExtensionSettings>
</template>
<template v-if="tab === 'otherSites'">
<OtherSiteSettings
v-if="settings"
:settings="settings"
>
</OtherSiteSettings>
</template>
<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> </div>
</template> </template>
<script> <script>
import SiteExtensionSettings from './PanelComponents/ExtensionSettings/SiteExtensionSettings.vue'; import SiteExtensionSettings from './PanelComponents/ExtensionSettings/SiteExtensionSettings.vue';
import FrameSiteSettings from './PanelComponents/ExtensionSettings/FrameSiteSettings.vue';
import OtherSiteSettings from './PanelComponents/ExtensionSettings/OtherSiteSettings.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 { export default {
data() {
components: { return {
SiteExtensionSettings, tab: 'siteSettings'
OtherSiteSettings, }
Popup,
ConfirmButton,
UploadJsonFileButton,
JsonEditor,
FrameSiteSettings,
}, },
mixins: [], mixins: [
],
props: [ props: [
'settings', 'settings',
'site', 'site',
'enableSettingsEditor',
'hosts',
], ],
data() { components: {
return { SiteExtensionSettings,
tab: 'siteSettings', OtherSiteSettings
importSettingDialogConfig: {visible: false}, },
allowSettingsEditing: false,
editorSaveFinished: false,
settingsJson: {},
settingsSnapshots: []
}
},
computed: { computed: {
globalSettings() { globalSettings() {
return this.settings?.getSiteSettings('@global') ?? null; return this.settings?.getSiteSettings('@global') ?? null;
@ -199,117 +89,17 @@ export default {
return this.settings?.getSiteSettings(this.site) ?? null; return this.settings?.getSiteSettings(this.site) ?? null;
} }
return null; return null;
}, }
},
mounted() {
this.resetSettingsEditor();
this.loadSettingsSnapshots();
}, },
methods: { methods: {
setTab(tab) { setTab(tab) {
this.tab = 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> </script>
<style lang="scss" src="../../res/css/flex.scss" scoped module></style> <style lang="scss" src="../../res/css/flex.scss" scoped module></style>
<style lang="scss" src="@csui/src/res-common/panels.scss" scoped module></style> <style lang="scss" src="../res-common/panels.scss" scoped module></style>
<style lang="scss" src="@csui/src/res-common/common.scss" scoped module></style> <style lang="scss" src="../res-common/common.scss" scoped module></style>

View File

@ -1,68 +1,113 @@
<template> <template>
<div class="flex flex-col w-full h-full gap-2"> <div class="flex flex-col w-full h-full gap-2">
<div class="flex flex-row gap-8 bg-black flex-wrap w-full"> <div class="flex flex-row gap-2 bg-black">
<div class="min-w-[400px] max-w-[520px] grow shrink"> <div class="w-[1/2]" style="width: 50%">
<h1>What's new</h1> <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> --> <p>Full changelog for older versions <a href="https://github.com/tamius-han/ultrawidify/blob/master/CHANGELOG.md">is available here</a>.</p>
<h2>6.3.0</h2> <h2>6.0.0</h2>
<p>
I don't think I need to write a changelog for this one. I've also been working on this, on and (mostly) off, for ... a long time, so I might have "kinda forgot" some minor things.
</p>
<ul> <ul>
<li>Automatic aspect ratio detection: do not apply negative aspect ratios</li> <li><b>Manifest v3</b>. Bit late, but still.</li>
<li>Keyboard zoom now works</li> <li>
<li><code>www.youtube-nocookie.com</code> has been added to the "officially supported" list</li> <b>In-player UI.</b><br/>
<li>Fixed the bug where UI would sometimes refuse to stay hidden</li> 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
<li>New experimental zoom options</li> if the video takes up sufficient amount of space (currently determined as 960 pixels wide).
<li>Subdomains now inherit same settings as their parent domain by default</li> </li>
<li>Extension attempts to detect embedded content.</li> <li>
<b>New alignment options.</b> Video can be aligned vertically as well as horizontally.
</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 should now display a warning whenever Ultrawidify detects automatic aspect ratio detection isn't happening because the site uses DRM.
</li>
<!-- <li>
Better zooming. The slider is back, baby.
</li> -->
<!-- <li>
Panning option that's a bit more intuitive.
</li> -->
</ul>
<h3>Unbaked features</h3>
<ul>
<li>
<b>Player UI settings</b>. Ultrawidify doesn't always correctly identify which area the video should fill, especially when not in full screen mode.
Version 6.0.0 was intended to come with UI that makes it easier for you to "help" ultrawidify by identifying the correct player element. Alas, I was
busier than expected and manifest v3 deadline came faster than expected.
</li>
<li>
<b>Disabling Ultrawidify UI trigger zone, or otherwise changing target area size and position, is not possible</b>.
</li>
</ul>
<h3>Regressions</h3>
<ul>
<li>
Due to major changes under the hood, custom settings from older versions were NOT been migrated (probably).
</li>
<li>
It is unclear how extension acts on sites with more than one video.
</li>
<li>
Extension lost the ability to discriminate between iframes. Actions taken through the popup will be affect <b>all</b> iframes.
</li>
<li>
Hope the UI is worth the regressions, because getting UI to work has been a MAJOR pain in the ass.
</li>
</ul> </ul>
</div> </div>
<div style="width: 1rem; height: 0px;"></div> <div class="w-[1/2]" style="width: 50%; padding-left: 1rem; padding-top: 5rem;">
<div class="min-w-[400px] max-w-[520px] grow shrink">
<h2>Report a problem</h2>
<p>
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>
<li>Email: <a target="_blank" :href="mailtoLink">tamius.han@gmail.com</a></li>
</ul>
<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> <h2>Thank you monies</h2>
<p> <p>
If you think I deserve money for the work I did up to this point, you can bankroll my caffeine addiction. If you think I deserve money for the work I did up to this point, you can bankroll my caffeine addiction.
</p> </p>
<p class="text-center"> <p class="text-center">
<a class="donate" href="https://www.paypal.com/paypalme/tamius" target="_blank">Donate on Paypal</a> <a class="donate" href="https://www.paypal.com/paypalme/tamius">Donate on Paypal</a>
</p>
<h2>Fun stuff</h2>
<p>
This is probably a bad idea but
</p>
<p>
Are you attending Isle of Wonders on Cres, Croatia, between 28. 6. and 30. 6.? So am I, by official duty.
</p>
<p>
Club Amulet D20 is forecasted to have a stand there, and I am forecasted to be in the general vicinity of it (barring any unexpected circumstances). I'll be either taking photos, painting minis, or doing heatstroke any% in rather rudamentary costume.
</p>
<p>
If you're there, you can swing around to say 'hi' or provide some validation, or paint some minis. Rumor has it Conquest will have paint&take event.
</p>
<p>
Tamius
</p>
<p>
</p>
<p>
<small>I am not paid to shill this.</small>
</p> </p>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import BrowserDetect from '@src/ext/conf/BrowserDetect';
export default({ export default({
props: [ props: [
'settings' '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() { mounted() {
this.settings.active.whatsNewChecked = true; this.settings.active.whatsNewChecked = true;
this.settings.saveWithoutReload(); this.settings.save();
} }
}); });
</script> </script>
@ -74,10 +119,6 @@ export default({
flex-direction: row; flex-direction: row;
} }
.grow {
flex-grow: 1;
}
p, li { p, li {
font-size: 1rem; font-size: 1rem;
} }

View File

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

View File

@ -0,0 +1,34 @@
<template>
<div>
<h1>In-player UI</h1>
<div
class="button b3"
style="margin: 16px; padding: 4px;"
@click="showInPlayerUi()"
>
Show in-player UI
</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',
],
methods: {
showInPlayerUi() {
this.eventBus.send('uw-set-ui-state', {globalUiVisible: true}, {comms: {forwardTo: 'active'}});
}
}
}
</script>

View File

@ -1,107 +0,0 @@
<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,51 +1,16 @@
<template> <template>
<div class=""> <div class="">
<template v-if="!selectedSite"> <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 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.
<b>NOTE:</b> Sites not on this list use default extension settings.
</div>
</div> </div>
<div class="w-full text-center" style="margin-bottom: -1.25rem"> <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;">
<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 flex-row">
<div class="flex-grow pointer"> <div class="flex-grow pointer">
<b>{{ site.key }}</b> <b>{{ site.key }}</b>
<span :style="getSiteTypeColor(site.type)"> <span :style="getSiteTypeColor(site.type)">
(config: {{site.type ?? 'unknown'}}) (config: {{site.type ?? 'unknown'}})
</span> </span></div>
</div>
<div>Edit</div> <div>Edit</div>
</div> </div>
<div class="flex flex-row"> <div class="flex flex-row">
@ -53,7 +18,6 @@
Enabled: <span :style="getSiteEnabledColor(site.key, 'enable')"><small>{{ getSiteEnabledModes(site.key, 'enable') }}</small></span>;&nbsp; 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; 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> 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> </small>
</div> </div>
</div> </div>
@ -64,7 +28,7 @@
</div> </div>
<div> <div>
Editing {{ selectedSite === '@global' ? 'default settings' : selectedSite }} Editing {{ selectedSite }}
</div> </div>
</div> </div>
<div> <div>
@ -72,7 +36,7 @@
v-if="selectedSiteSettings" v-if="selectedSiteSettings"
:settings="settings" :settings="settings"
:siteSettings="selectedSiteSettings" :siteSettings="selectedSiteSettings"
:isDefaultConfiguration="selectedSite === '@global'" :isDefaultConfiguration="false"
></SiteExtensionSettings> ></SiteExtensionSettings>
</div> </div>
</template> </template>
@ -87,8 +51,6 @@ export default {
data() { data() {
return { return {
selectedSite: null, selectedSite: null,
siteFilter: '',
filteredSites: []
} }
}, },
props: [ props: [
@ -104,7 +66,7 @@ export default {
} else { } else {
const sites = []; const sites = [];
for (const siteKey in this.settings.active.sites) { for (const siteKey in this.settings.active.sites) {
if (!siteKey.startsWith('@') && (!this.siteFilter.trim() || siteKey.includes(this.siteFilter))) { if (!siteKey.startsWith('@')) {
sites.push({ sites.push({
key: siteKey, key: siteKey,
...this.settings.active.sites[siteKey] ...this.settings.active.sites[siteKey]
@ -156,17 +118,5 @@ export default {
</script> </script>
<style lang="scss" src="../../../../res/css/flex.scss" scoped></style> <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="../../../res-common/panels.scss" scoped></style>
<style lang="scss" src="@csui/src/res-common/common.scss" scoped></style> <style lang="scss" 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

@ -3,12 +3,11 @@
<!-- Enable extension --> <!-- Enable extension -->
<div class="field"> <div class="field">
<div class="label"> <div class="label">
Enable <span class="color-emphasis">extension</span> Enable extension under the following conditions:
<span class="sub-label"><br/>under the following conditions:</span>
</div> </div>
<div class="select"> <div class="select">
<select <select
:value="simpleExtensionSettings.enable" v-model="simpleExtensionSettings.enable"
@click="setExtensionMode('enable', $event)" @click="setExtensionMode('enable', $event)"
> >
<option <option
@ -19,12 +18,12 @@
</option> </option>
<template v-if="isDefaultConfiguration"> <template v-if="isDefaultConfiguration">
<option value="disabled"> <option value="disabled">
Disabled by default Disabled (unless enabled for specific site)
</option> </option>
</template> </template>
<template v-else> <template v-else>
<option value="default"> <option value="default">
Use default ({{simpleDefaultSettings.enable}}) Use default ()
</option> </option>
<option value="disabled"> <option value="disabled">
Never Never
@ -43,239 +42,199 @@
</div> </div>
</div> </div>
<!-- The rest of the menu is disabled when extension is disabled --> <!-- Enable AARD -->
<div :class="{disabled: simpleEffectiveSettings.enable === 'disabled' && !isDefaultConfiguration}"> <div class="field">
<!-- Enable AARD --> <div class="label">
<div class="field"> Enable automatic aspect ratio detection under the following conditions:
<div class="label">
Enable <span class="color-emphasis">automatic aspect ratio detection</span>
<span class="sub-label"><br/>under the following conditions:</span>
</div>
<div class="select">
<select
:value="simpleExtensionSettings.enableAard"
@click="setExtensionMode('enableAard', $event)"
>
<option
v-if="simpleExtensionSettings.enable === 'complex'"
value="complex"
>
(Site uses advanced settings)
</option>
<template v-if="isDefaultConfiguration">
<option value="disabled">
Disabled by default
</option>
</template>
<template v-else>
<option value="default">
Use default ({{simpleDefaultSettings.enableAard}})
</option>
<option value="disabled">
Never
</option>
</template>
<option value="fs">
Fullscreen only
</option>
<option value="theater">
Fullscreen and theater mode
</option>
<option value="enabled">
Always
</option>
</select>
</div>
</div> </div>
<div class="select">
<!-- Enable keyboard --> <select
<div class="field"> v-model="simpleExtensionSettings.enableAard"
<div class="label"> @click="setExtensionMode('enableAard', $event)"
Enable <span class="color-emphasis">keyboard shortcuts</span> >
<span class="sub-label"><br/>under the following conditions:</span> <option
</div> v-if="simpleExtensionSettings.enable === 'complex'"
<div class="select"> value="complex"
<select
:value="simpleExtensionSettings.enableKeyboard"
@click="setExtensionMode('enableKeyboard', $event)"
> >
<option (Site uses advanced settings)
v-if="simpleExtensionSettings.enable === 'complex'" </option>
value="complex" <template v-if="isDefaultConfiguration">
> <option value="disabled">
(Site uses advanced settings) Disabled (unless enabled for specific site)
</option> </option>
<template v-if="isDefaultConfiguration"> </template>
<option value="disabled"> <template v-else>
Disabled by default <option value="default">
</option> Use default ()
</template>
<template v-else>
<option value="default">
Use default ({{simpleDefaultSettings.enableKeyboard}})
</option>
<option value="disabled">
Never
</option>
</template>
<option value="fs">
Fullscreen only
</option> </option>
<option value="theater"> <option value="disabled">
Fullscreen and theater mode Never
</option> </option>
<option value="enabled"> </template>
Always <option value="fs">
</option> Fullscreen only
</select> </option>
</div> <option value="theater">
Fullscreen and theater mode
</option>
<option value="enabled">
Always
</option>
</select>
</div> </div>
</div>
<!-- Enable UI --> <!-- Enable keyboard -->
<div class="field"> <div class="field">
<div class="label"> <div class="label">
Enable <span class="color-emphasis">in-player UI</span> Enable keyboard shortcuts under the following conditions
<span class="sub-label"><br/>under the following conditions:</span>
</div>
<div class="select">
<select
:value="simpleExtensionSettings.enableUI"
@click="setExtensionMode('enableUI', $event)"
>
<template v-if="isDefaultConfiguration">
<option value="disabled">
Disabled by default
</option>
</template>
<template v-else>
<option value="default">
Use default ({{simpleDefaultSettings.enableUI}})
</option>
<option value="disabled">
Never
</option>
</template>
<option value="fs">
Fullscreen only
</option>
<option value="theater">
Always where possible
</option>
</select>
</div>
</div> </div>
<div class="select">
<!-- Default crop --> <select
<div class="field"> v-model="simpleExtensionSettings.enableKeyboard"
<div class="label">Default crop:</div> @click="setExtensionMode('enableKeyboard', $event)"
<div class="select"> >
<select <option
:value="siteDefaultCrop" v-if="simpleExtensionSettings.enable === 'complex'"
@change="setOption('defaults.crop', $event)" value="complex"
> >
<option (Site uses advanced settings)
v-if="!isDefaultConfiguration" </option>
:value="JSON.stringify({useDefault: true})" <template v-if="isDefaultConfiguration">
> <option value="disabled">
Use default ({{getCommandValue(settings?.active.commands.crop, siteSettings.data.defaults.crop)}}) Disabled (unless enabled for specific site)
</option> </option>
<option </template>
v-for="(command, index) of settings?.active.commands.crop" <template v-else>
:key="index" <option value="default">
:value="JSON.stringify(command.arguments)" Use default ()
>
{{command.label}}
</option> </option>
</select> <option value="disabled">
</div> Never
</option>
</template>
<option value="fs">
Fullscreen only
</option>
<option value="theater">
Fullscreen and theater mode
</option>
<option value="enabled">
Always
</option>
</select>
</div>
</div>
<!-- Default crop -->
<div class="field">
<div class="label">Default crop:</div>
<div class="select">
<select
v-model="siteDefaultCrop"
@click="setOption('defaults.crop', $event)"
>
<option
v-if="!isDefaultConfiguration"
:value="undefined"
>
Use default ({{getCommandValue(settings?.active.commands.crop, siteSettings.data.defaults.crop)}})
</option>
<option
v-for="(command, index) of settings?.active.commands.crop"
:key="index"
:value="JSON.stringify(command.arguments)"
>
{{command.label}}
</option>
</select>
</div> </div>
<div class="hint">This is how extension will crop video if/when autodetection is disabled. Pick 'Reset' option to keep aspect ratio as-is by default.</div> <div class="hint">This is how extension will crop video if/when autodetection is disabled. Pick 'Reset' option to keep aspect ratio as-is by default.</div>
</div>
<!-- Default stretch --> <!-- Default stretch -->
<div class="field"> <div class="field">
<div class="label">Default stretch:</div> <div class="label">Default stretch:</div>
<div class="select"> <div class="select">
<select <select
v-model="siteDefaultStretch" v-model="siteDefaultStretch"
@change="setOption('defaults.stretch', $event)" @click="setOption('defaults.stretch', $event)"
>
<option
v-if="!isDefaultConfiguration"
:value="undefined"
> >
<option Use default ({{getCommandValue(settings?.active.commands.stretch, siteSettings.data.defaults.stretch)}})
v-if="!isDefaultConfiguration" </option>
:value="JSON.stringify({useDefault: true})" <option
> v-for="(command, index) of settings?.active.commands.stretch"
Use default ({{getCommandValue(settings?.active.commands.stretch, siteSettings.data.defaults.stretch)}}) :key="index"
</option> :value="JSON.stringify(command.arguments)"
<option >
v-for="(command, index) of settings?.active.commands.stretch" {{command.label}}
:key="index" </option>
:value="JSON.stringify(command.arguments)" </select>
>
{{command.label}}
</option>
</select>
</div>
</div> </div>
</div>
<!-- Default alignment --> <!-- Default alignment -->
<div class="field"> <div class="field">
<div class="label">Default alignment:</div> <div class="label">Default alignment:</div>
<div class="select"> <div class="select">
<select <select
v-model="siteDefaultAlignment" v-model="siteDefaultAlignment"
@change="setOption('defaults.alignment', $event)" @click="setOption('defaults.alignment', $event)"
>
<option
v-if="!isDefaultConfiguration"
:value="undefined"
> >
<option Use default ({{getAlignmentLabel(siteSettings.data.defaults.alignment)}})
v-if="!isDefaultConfiguration" </option>
:value="JSON.stringify({useDefault: true})" <option
> v-for="(command, index) of alignmentOptions"
Use default ({{getAlignmentLabel(siteSettings.data.defaults.alignment)}}) :key="index"
</option> :value="JSON.stringify(command.arguments)"
<option >
v-for="(command, index) of alignmentOptions" {{command.label}}
:key="index" </option>
:value="JSON.stringify(command.arguments)" </select>
>
{{command.label}}
</option>
</select>
</div>
</div> </div>
</div>
<!-- Crop, et. al. Persistence --> <!-- Crop, et. al. Persistence -->
<div class="field"> <div class="field">
<div class="label">Persist crop, stretch, and alignment between videos</div> <div class="label">Persist crop, stretch, and alignment between videos</div>
<div class="select"> <div class="select">
<select <select
v-model="siteDefaultCropPersistence" v-model="siteDefaultCropPersistence"
@click="setOption('persistCSA', $event)" @click="setOption('persistCSA', $event)"
>
<option
v-if="!isDefaultConfiguration"
:value="CropModePersistence.Default"
> >
<option Use default ({{defaultPersistanceLabel}})
v-if="!isDefaultConfiguration" </option>
:value="CropModePersistence.Default" <option :value="CropModePersistence.Disabled">Disabled</option>
> <option :value="CropModePersistence.UntilPageReload">Until page reload</option>
Use default ({{defaultPersistanceLabel}}) <option :value="CropModePersistence.CurrentSession">Current session</option>
</option> <option :value="CropModePersistence.Forever">Always persist</option>
<option :value="CropModePersistence.Disabled">Disabled</option> </select>
<option :value="CropModePersistence.UntilPageReload">Until page reload</option>
<option :value="CropModePersistence.CurrentSession">Current session</option>
<option :value="CropModePersistence.Forever">Always persist</option>
</select>
</div>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import ExtensionMode from '@src/common/enums/ExtensionMode.enum'; import ExtensionMode from '../../../../../common/enums/ExtensionMode.enum';
import VideoAlignmentType from '@src/common/enums/VideoAlignmentType.enum'; import VideoAlignmentType from '../../../../../common/enums/VideoAlignmentType.enum';
import CropModePersistence from '@src/common/enums/CropModePersistence.enum'; import CropModePersistence from './../../../../../common/enums/CropModePersistence.enum';
export default { export default {
data() { data() {
return { return {
CropModePersistence: CropModePersistence, CropModePersistence: CropModePersistence,
ExtensionMode,
alignmentOptions: [ alignmentOptions: [
{label: 'Top left', arguments: {x: VideoAlignmentType.Left, y: VideoAlignmentType.Top}}, {label: 'Top left', arguments: {x: VideoAlignmentType.Left, y: VideoAlignmentType.Top}},
{label: 'Top center', arguments: {x: VideoAlignmentType.Center, y: VideoAlignmentType.Top}}, {label: 'Top center', arguments: {x: VideoAlignmentType.Center, y: VideoAlignmentType.Top}},
@ -306,33 +265,16 @@ export default {
enable: this.compileSimpleSettings('enable'), enable: this.compileSimpleSettings('enable'),
enableAard: this.compileSimpleSettings('enableAard'), enableAard: this.compileSimpleSettings('enableAard'),
enableKeyboard: this.compileSimpleSettings('enableKeyboard'), enableKeyboard: this.compileSimpleSettings('enableKeyboard'),
enableUI: this.compileSimpleSettings('enableUI')
} }
}, },
simpleEffectiveSettings() {
return {
enable: this.compileSimpleSettings('enable', 'site-effective'),
enableAard: this.compileSimpleSettings('enableAard', 'site-effective'),
enableKeyboard: this.compileSimpleSettings('enableKeyboard', 'site-effective'),
enableUI: this.compileSimpleSettings('enableUI', 'site-effective')
}
},
simpleDefaultSettings() {
return {
enable: this.getDefaultOptionLabel('enable'),
enableAard: this.getDefaultOptionLabel('enableAard'),
enableKeyboard: this.getDefaultOptionLabel('enableKeyboard'),
enableUI: this.getDefaultOptionLabel('enableUI')
};
},
siteDefaultCrop() { siteDefaultCrop() {
return this.siteSettings.raw?.defaults?.crop ? JSON.stringify(this.siteSettings.raw?.defaults?.crop) : JSON.stringify({useDefault: true}); return this.siteSettings.raw?.defaults?.crop ? JSON.stringify(this.siteSettings.raw?.defaults?.crop) : undefined;
}, },
siteDefaultStretch() { siteDefaultStretch() {
return this.siteSettings.raw?.defaults?.stretch ? JSON.stringify(this.siteSettings.raw?.defaults?.stretch) : JSON.stringify({useDefault: true}); return this.siteSettings.raw?.defaults?.stretch ? JSON.stringify(this.siteSettings.raw?.defaults?.stretch) : undefined;
}, },
siteDefaultAlignment() { siteDefaultAlignment() {
return this.siteSettings.raw?.defaults?.alignment ? JSON.stringify(this.siteSettings.raw?.defaults?.alignment) : JSON.stringify({useDefault: true}); return this.siteSettings.raw?.defaults?.alignment ? JSON.stringify(this.siteSettings.raw?.defaults?.alignment) : undefined;
}, },
siteDefaultCropPersistence() { siteDefaultCropPersistence() {
return this.siteSettings.raw?.persistCSA ?? undefined; return this.siteSettings.raw?.persistCSA ?? undefined;
@ -352,71 +294,44 @@ export default {
return '??'; return '??';
} }
}, },
mounted() {
this.forceRefreshPage();
},
methods: { methods: {
/** /**
* Compiles our extension settings into more user-friendly options * Compiles our extension settings into more user-friendly options
*/ */
compileSimpleSettings(component, getFor = 'site') { compileSimpleSettings(component) {
let settingsData;
switch (getFor) {
case 'site':
settingsData = this.siteSettings?.raw;
break;
case 'site-effective':
settingsData = this.siteSettings?.data;
break;
case 'default':
settingsData = this.settings.active.sites['@global'];
break;
}
// console.log('getting data from:', settingsData);
try { try {
if ( if (
( settingsData?.[component]?.normal === ExtensionMode.Disabled || component === 'enableUI') this.siteSettings?.data?.[component]?.normal === ExtensionMode.Disabled
&& settingsData?.[component]?.theater === ExtensionMode.Disabled && this.siteSettings?.data?.[component]?.theater === ExtensionMode.Disabled
&& settingsData?.[component]?.fullscreen === ExtensionMode.Disabled && this.siteSettings?.data?.[component]?.fullscreen === ExtensionMode.Disabled
) { ) {
return 'disabled'; return 'disabled';
} }
if ( if (
( settingsData?.[component]?.normal === ExtensionMode.Default || component === 'enableUI') this.siteSettings?.data?.[component]?.normal === ExtensionMode.Default
&& settingsData?.[component]?.theater === ExtensionMode.Default && this.siteSettings?.data?.[component]?.theater === ExtensionMode.Default
&& settingsData?.[component]?.fullscreen === ExtensionMode.Default && this.siteSettings?.data?.[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}`,
// `\n\n(expected values:`, ExtensionMode.Default
// )
return 'default'; return 'default';
} }
if ( if (
( settingsData?.[component]?.normal === ExtensionMode.Disabled || component === 'enableUI') this.siteSettings?.data?.[component]?.normal === ExtensionMode.Disabled
&& settingsData?.[component]?.theater === ExtensionMode.Disabled && this.siteSettings?.data?.[component]?.theater === ExtensionMode.Disabled
&& settingsData?.[component]?.fullscreen === ExtensionMode.Enabled && this.siteSettings?.data?.[component]?.fullscreen === ExtensionMode.Enabled
) { ) {
return 'fs'; return 'fs';
} }
if ( if (
( settingsData?.[component]?.normal === ExtensionMode.Disabled || component === 'enableUI') this.siteSettings?.data?.[component]?.normal === ExtensionMode.Disabled
&& settingsData?.[component]?.theater === ExtensionMode.Enabled && this.siteSettings?.data?.[component]?.theater === ExtensionMode.Enabled
&& settingsData?.[component]?.fullscreen === ExtensionMode.Enabled && this.siteSettings?.data?.[component]?.fullscreen === ExtensionMode.Enabled
) { ) {
return 'theater'; return 'theater';
} }
if ( if (
( settingsData?.[component]?.normal === ExtensionMode.Enabled || component === 'enableUI') this.siteSettings?.data?.[component]?.normal === ExtensionMode.Enabled
&& settingsData?.[component]?.theater === ExtensionMode.Enabled && this.siteSettings?.data?.[component]?.theater === ExtensionMode.Enabled
&& settingsData?.[component]?.fullscreen === ExtensionMode.Enabled && this.siteSettings?.data?.[component]?.fullscreen === ExtensionMode.Enabled
) { ) {
return 'enabled'; return 'enabled';
} }
@ -427,37 +342,6 @@ export default {
} }
}, },
getDefaultOptionLabel(component) {
const componentValue = this.compileSimpleSettings(component, 'default');
if (componentValue === 'loading') {
return componentValue;
}
if (component === 'enableUI') {
switch (componentValue) {
case 'fs':
return 'fullscreen only';
case 'enabled':
case 'theater':
return 'where possible';
case 'disabled':
return 'disabled';
}
}
switch (componentValue) {
case 'fs':
return 'Fullscreen only';
case 'theater':
return 'theater & FS';
case 'enabled':
return 'always';
case 'disabled':
return 'disabled';
case 'default':
return 'complex'
}
},
getCommandValue(availableCommands, command) { getCommandValue(availableCommands, command) {
for (const cmd of availableCommands) { for (const cmd of availableCommands) {
if (JSON.stringify(cmd.arguments) === JSON.stringify(command)) { if (JSON.stringify(cmd.arguments) === JSON.stringify(command)) {
@ -502,60 +386,25 @@ export default {
getOption(option) { getOption(option) {
}, },
async setOption(option, $event) { setOption(option, $event) {
const value = $event.target.value;
let commandArguments; let commandArguments;
// if argument is json, parse json. Otherwise, pass the value as-is // if argument is json, parse json. Otherwise, pass the value as-is
try { try {
commandArguments = value !== undefined ? JSON.parse(value) : undefined; commandArguments = $event.target.value !== undefined ? JSON.parse($event.target.value) : undefined;
} catch(e) { } catch(e) {
commandArguments = value; commandArguments = $event.target.value;
} }
if (commandArguments.useDefault) { this.siteSettings.set(option, commandArguments);
commandArguments = undefined;
}
await this.siteSettings.set(option, commandArguments, {reload: false});
// 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();
this._computedWatchers?.siteDefaultStretch?.run();
this._computedWatchers?.siteDefaultAlignment?.run();
this._computedWatchers?.siteDefaultCropPersistence?.run();
this._computedWatchers?.defaultPersistanceLabel?.run();
this.$nextTick( () => this.$forceUpdate());
},
setExtensionMode(component, event) { setExtensionMode(component, event) {
const option = event.target.value; const option = event.target.value;
console.log('SET EXTENSION MODE — OPTIONS:', option);
if (option === 'complex') { if (option === 'complex') {
return; return;
} }
if (component === 'enable' && !this.isDefaultConfiguration) {
this.setExtensionMode('enableAard', event);
this.setExtensionMode('enableKeyboard', event);
// in enableUI, 'enabled' is unused and 'theater' uses its place
if (option === 'enabled') {
this.setExtensionMode('enableUI', {target: {value: 'theater'}});
} else {
this.setExtensionMode('enableUI', event);
}
}
if (option === 'default') { if (option === 'default') {
return this.siteSettings.set(component, { return this.siteSettings.set(component, {
normal: ExtensionMode.Default, normal: ExtensionMode.Default,
@ -598,8 +447,8 @@ export default {
</script> </script>
<style lang="scss" src="../../../../res/css/flex.scss" scoped></style> <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="../../../res-common/panels.scss" scoped></style>
<style lang="scss" src="@csui/src/res-common/common.scss" scoped></style> <style lang="scss" src="../../../res-common/common.scss" scoped></style>
<style scoped> <style scoped>
.button-hover:hover { .button-hover:hover {
color: #fa6; color: #fa6;

View File

@ -1,71 +0,0 @@
<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

@ -1,14 +1,13 @@
<template> <template>
<!-- <div class="flex flex-row">
<mdicon name="crop" :size="32" />
<h1>Crop video:</h1>
</div> -->
<div class="sub-panel-content flex flex-row flex-wrap"> <div class="sub-panel-content flex flex-row flex-wrap">
<ShortcutButton <ShortcutButton
v-for="(command, index) of settings?.active.commands.crop" v-for="(command, index) of settings?.active.commands.crop"
class="flex button" class="flex b3 button"
:class="{ :class="{active: editMode ? index === editModeOptions?.crop?.selectedIndex : isActiveCrop(command)}"
active: editMode ? index === editModeOptions?.crop?.selectedIndex : isActiveCrop(command),
'b3-compact': compact,
b3: !compact
}"
:key="index" :key="index"
:label="command.label" :label="command.label"
:shortcut="getKeyboardShortcutLabel(command)" :shortcut="getKeyboardShortcutLabel(command)"
@ -58,13 +57,12 @@
@blur="editModeOptions.crop.selected.label === 'New aspect ratio' ? editModeOptions.crop.selected.label = editModeOptions.crop.selected.arguments.ratio : null" @blur="editModeOptions.crop.selected.label === 'New aspect ratio' ? editModeOptions.crop.selected.label = editModeOptions.crop.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>
<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="field">
<div class="label"> <div class="label">
Label: Label:
@ -72,9 +70,9 @@
<div class="input"> <div class="input">
<input v-model="editModeOptions.crop.selected.label"> <input v-model="editModeOptions.crop.selected.label">
</div> </div>
</div> <div class="hint">
<div class="hint"> Label for the button. You can make it say something other than ratio.
Label for the button. You can make it say something other than ratio. </div>
</div> </div>
</template> </template>
@ -88,11 +86,11 @@
> >
</EditShortcutButton> </EditShortcutButton>
</div> </div>
</div> <div class="hint">
<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.
<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
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.
any keyboard shortcuts defined by the site itself. </div>
</div> </div>
<div class="flex flex-row flex-end"> <div class="flex flex-row flex-end">
@ -116,13 +114,13 @@
</div> </div>
</div> </div>
<div v-if="siteSettings && allowSettingSiteDefault" class="edit-action-area"> <div class="edit-action-area">
<div class="field"> <div class="field">
<div class="label">Default for this site</div> <div class="label">Default for this site</div>
<div class="select"> <div class="select">
<select <select
:value="siteDefaultCrop" :value="siteDefaultCrop"
@change="setDefaultCrop($event, 'site')" @click="setDefaultCrop($event, 'site')"
> >
<option <option
v-for="(command, index) of settings?.active.commands.crop" v-for="(command, index) of settings?.active.commands.crop"
@ -138,25 +136,16 @@
</template> </template>
<script> <script>
import ShortcutButton from '@csui/src/components/ShortcutButton.vue'; import ShortcutButton from '../../../components/ShortcutButton.vue';
import EditShortcutButton from '@csui/src/components/EditShortcutButton'; import EditShortcutButton from '../../../components/EditShortcutButton';
import EditModeMixin from '@csui/src/utils/EditModeMixin'; import EditModeMixin from '../../../utils/EditModeMixin';
import KeyboardShortcutParserMixin from '@csui/src/utils/KeyboardShortcutParserMixin'; import KeyboardShortcutParserMixin from '../../../utils/KeyboardShortcutParserMixin';
import CommsMixin from '@csui/src/utils/CommsMixin'; import CommsMixin from '../../../utils/CommsMixin';
import AspectRatioType from '@src/common/enums/AspectRatioType.enum'; import AspectRatioType from '../../../../../common/enums/AspectRatioType.enum';
export default { export default {
components: {
ShortcutButton,
EditShortcutButton,
},
mixins: [
// ComputeActionsMixin,
EditModeMixin,
KeyboardShortcutParserMixin,
CommsMixin
],
data() { data() {
return { return {
AspectRatioType: AspectRatioType, AspectRatioType: AspectRatioType,
@ -169,29 +158,29 @@ export default {
} }
} }
}, },
mixins: [
// ComputeActionsMixin,
EditModeMixin,
KeyboardShortcutParserMixin,
CommsMixin
],
props: [ props: [
'settings', // required for buttons and actions, which are global 'settings', // required for buttons and actions, which are global
'siteSettings', 'siteSettings',
'eventBus', 'eventBus',
'isEditing', 'isEditing'
'allowSettingSiteDefault',
'compact',
], ],
components: {
ShortcutButton,
EditShortcutButton,
},
computed: { computed: {
siteDefaultCrop() { siteDefaultCrop() {
if (!this.siteSettings) {
return null;
}
return JSON.stringify( return JSON.stringify(
this.siteSettings.data.defaults.crop this.siteSettings.data.defaults.crop
); );
}, },
}, },
created() {
if (this.isEditing) {
this.enableEditMode();
}
},
watch: { watch: {
isEditing(newValue, oldValue) { isEditing(newValue, oldValue) {
if (newValue) { if (newValue) {
@ -206,9 +195,6 @@ export default {
* Sets default crop, for either site or global * Sets default crop, for either site or global
*/ */
setDefaultCrop($event, scope) { setDefaultCrop($event, scope) {
if (!this.siteSettings) {
return;
}
const commandArguments = JSON.parse($event.target.value); const commandArguments = JSON.parse($event.target.value);
this.siteSettings.set('defaults.crop', commandArguments); this.siteSettings.set('defaults.crop', commandArguments);
@ -219,7 +205,7 @@ export default {
* Determines whether a given crop command is the currently active one * Determines whether a given crop command is the currently active one
*/ */
isActiveCrop(cropCommand) { isActiveCrop(cropCommand) {
if (! this.resizerConfig.crop || !this.siteSettings) { if (! this.resizerConfig.crop) {
return false; return false;
} }
@ -246,5 +232,5 @@ export default {
</script> </script>
<style lang="scss" src="../../../../res/css/flex.scss" scoped></style> <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="../../../res-common/panels.scss" scoped></style>
<style lang="scss" src="@csui/src/res-common/common.scss" scoped></style> <style lang="scss" src="../../../res-common/common.scss" scoped></style>

View File

@ -3,10 +3,7 @@
<ShortcutButton <ShortcutButton
v-for="(command, index) of settings?.active.commands.stretch" v-for="(command, index) of settings?.active.commands.stretch"
class="b3 button" 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" :key="index"
:label="command.label" :label="command.label"
:shortcut="getKeyboardShortcutLabel(command)" :shortcut="getKeyboardShortcutLabel(command)"
@ -53,10 +50,10 @@
v-model="editModeOptions.stretch.selected.arguments.limit" v-model="editModeOptions.stretch.selected.arguments.limit"
> >
</div> </div>
</div> <div class="hint">
<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.
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.
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> </div>
</template> </template>
@ -73,12 +70,12 @@
@blur="editModeOptions.stretch.selected.label === 'Stretch to ...' ? editModeOptions.stretch.selected.label = `Stretch to ${editModeOptions.stretch.selected.arguments.ratio}` : null" @blur="editModeOptions.stretch.selected.label === 'Stretch to ...' ? editModeOptions.stretch.selected.label = `Stretch to ${editModeOptions.stretch.selected.arguments.ratio}` : null"
> >
</div> </div>
</div> <div class="hint">
<div class="hint">
You can enter a ratio in width:height format (e.g. "21:9" or "1:2.39"), or just the factor 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 (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. your numbers without quote marks. Number will be converted to factor form on save.
</div> </div>
</div>
<div class="field"> <div class="field">
<div class="label"> <div class="label">
Label: Label:
@ -86,9 +83,9 @@
<div class="input"> <div class="input">
<input v-model="editModeOptions.stretch.selected.label"> <input v-model="editModeOptions.stretch.selected.label">
</div> </div>
</div> <div class="hint">
<div class="hint"> Label for the button. You can make it say something other than ratio.
Label for the button. You can make it say something other than ratio. </div>
</div> </div>
</template> </template>
@ -102,11 +99,11 @@
> >
</EditShortcutButton> </EditShortcutButton>
</div> </div>
</div> <div class="hint">
<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.
<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
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.
any keyboard shortcuts defined by the site itself. </div>
</div> </div>
<div class="flex flex-row flex-end"> <div class="flex flex-row flex-end">
@ -130,14 +127,14 @@
</div> </div>
</div> </div>
<div v-if="siteSettings && allowSettingSiteDefault" class="edit-action-area"> <div class="edit-action-area">
<div class="field"> <div class="field">
<div class="label">Default for this site:</div> <div class="label">Default for this site:</div>
<div class="select"> <div class="select">
<div class="select"> <div class="select">
<select <select
v-model="siteDefaultStretchMode" v-model="siteDefaultStretchMode"
@change="setDefaultStretchingMode($event, 'site')" @click="setDefaultStretchingMode($event, 'site')"
> >
<option <option
v-for="(command, index) of settings?.active.commands.stretch" v-for="(command, index) of settings?.active.commands.stretch"
@ -186,9 +183,7 @@ export default {
'settings', // required for buttons and actions, which are global 'settings', // required for buttons and actions, which are global
'siteSettings', 'siteSettings',
'eventBus', 'eventBus',
'isEditing', 'isEditing'
'allowSettingSiteDefault',
'compact',
], ],
components: { components: {
ShortcutButton, ShortcutButton,
@ -196,19 +191,11 @@ export default {
}, },
computed: { computed: {
siteDefaultStretch() { siteDefaultStretch() {
if (!this.siteSettings) {
return null;
}
return JSON.stringify( return JSON.stringify(
this.siteSettings.data.defaults.stretch this.siteSettings.data.defaults.stretch
); );
}, },
}, },
created() {
if (this.isEditing) {
this.enableEditMode();
}
},
watch: { watch: {
isEditing(newValue, oldValue) { isEditing(newValue, oldValue) {
if (newValue) { if (newValue) {
@ -223,9 +210,6 @@ export default {
* Sets default stretching mode, for either site or global * Sets default stretching mode, for either site or global
*/ */
setDefaultStretchingMode($event, globalOrSite) { setDefaultStretchingMode($event, globalOrSite) {
if (!this.siteSettings) {
return;
}
const commandArguments = JSON.parse($event.target.value); const commandArguments = JSON.parse($event.target.value);
this.siteSettings.set('defaults.stretch', commandArguments); this.siteSettings.set('defaults.stretch', commandArguments);
}, },
@ -234,7 +218,7 @@ export default {
* Determines whether a given stretch command is the currently active one * Determines whether a given stretch command is the currently active one
*/ */
isActiveStretch(stretchCommand) { isActiveStretch(stretchCommand) {
if (! this.resizerConfig.stretch || !this.siteSettings) { if (! this.resizerConfig.stretch) {
return false; return false;
} }
@ -249,6 +233,6 @@ export default {
} }
</script> </script>
<style lang="scss" src="@csui/res/css/flex.scss" scoped></style> <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="../../../res-common/panels.scss" scoped></style>
<style lang="scss" src="@csui/src/res-common/common.scss" scoped></style> <style lang="scss" src="../../../res-common/common.scss" scoped></style>

View File

@ -1,224 +1,86 @@
<template> <template>
<div class="flex flex-col"> <div class="flex flex-col">
<!--
<div class="sub-panel-content flex flex-row flex-wrap"> min, max and value need to be implemented in js as this slider
should use logarithmic scale
<ShortcutButton -->
v-for="(command, index) of settings?.active.commands.zoom" <div class="flex flex-row flex-end">
class="flex button" <Button
:class="{active: editMode ? index === editModeOptions?.zoom?.selectedIndex : isActiveZoom(command), v-if="zoomAspectRatioLocked"
'b3-compact': compact, label="Unlock aspect ratio"
b3: !compact icon="lock-open"
}" :fixedWidth="true"
:key="index" @click="toggleZoomAr()"
:label="command.label"
:shortcut="getKeyboardShortcutLabel(command)"
@click="editMode ? editAction(command, index, 'zoom') : execAction(command)"
> >
</ShortcutButton> </Button>
<Button
<!-- "Add new" button --> v-else
<ShortcutButton label="Lock aspect ratio"
v-if="editMode" icon="lock"
class="button b3" :fixedWidth="true"
:class="{active: editMode ? editModeOptions?.crop?.selectedIndex === null : isActiveCrop(command)}" @click="toggleZoomAr()"
label="Add new" >
@click="editAction( </Button>
{action: 'set-ar-zoom', label: 'New aspect ratio', arguments: {type: AspectRatioType.Fixed}},
null,
'zoom'
)"
></ShortcutButton>
</div> </div>
<template v-if="zoomAspectRatioLocked">
<template v-if="isEditing"> <input id="_input_zoom_slider"
<div v-if="editMode && editModeOptions?.zoom?.selected" class="sub-panel-content"> class="input-slider"
<div class="edit-action-area-header"> type="range"
<span class="text-primary">Editing options for:</span> <b>{{editModeOptions?.zoom?.selected?.label}}</b>&nbsp; step="any"
<template v-if="editModeOptions?.zoom?.selectedIndex === null && editModeOptions?.zoom?.selected?.label !== 'New aspect ratio'">(New ratio)</template> min="-1"
max="3"
: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>
<div class="edit-action-area"> <div class="flex flex-nogrow flex-noshrink medium-small">
<!-- Some options are only shown for type 4 (fixed) zooms --> <a class="_zoom_reset x-pad-1em" @click="resetZoom()">reset</a>
<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>
</div> </div>
</template> </template>
<template v-else> <template v-else>
<!-- <div>Horizontal zoom</div>
min, max and value need to be implemented in js as this slider <input id="_input_zoom_slider"
should use logarithmic scale class="input-slider"
--> type="range"
<div class="flex flex-row w-full" style="margin-top: 0.66rem"> step="any"
<div style="position:relative;" class="grow"> min="-1"
<template v-if="zoomAspectRatioLocked"> max="4"
<div class="slider-label"> :value="zoom.x"
Zoom: {{getZoomForDisplay('x')}} @input="changeZoom($event.target.value, 'x')"
</div> />
<input id="_input_zoom_slider"
class="input-slider"
type="range"
step="any"
min="-1"
max="3"
:value="zoom.x"
@input="changeZoom($event.target.value)"
/>
</template>
<template v-else>
<div class="slider-label">Horizontal zoom: {{getZoomForDisplay('x')}}</div>
<input id="_input_zoom_slider"
class="input-slider"
type="range"
step="any"
min="-1"
max="4"
:value="zoom.x"
@input="changeZoom($event.target.value, 'x')"
/>
<div class="slider-label">Vertical zoom: {{getZoomForDisplay('y')}}</div> <div>Vertical zoom</div>
<input id="_input_zoom_slider_2" <input id="_input_zoom_slider"
class="input-slider" class="input-slider"
type="range" type="range"
step="any" step="any"
min="-1" min="-1"
max="3" max="3"
:value="zoom.y" :value="zoom.y"
@input="changeZoom($event.target.value, 'y')" @input="changeZoom($event.target.value, 'y')"
/> />
</template>
<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>
<div class="flex flex-nogrow flex-noshrink medium-small">
<div class="flex flex-row items-center justify-center" style="padding-left: 1rem"> <a class="_zoom_reset x-pad-1em" @click="resetZoom()">reset</a>
<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> </div>
</template> </template>
</div> </div>
</template> </template>
<script> <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 { 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() { data() {
return { return {
AspectRatioType,
zoomAspectRatioLocked: true, zoomAspectRatioLocked: true,
zoom: { zoom: {
x: 0, x: 0,
@ -234,24 +96,20 @@ export default {
} }
} }
}, },
created() { mixins: [
if (this.isEditing) {
this.enableEditMode(); ],
} props: [
}, 'settings', // required for buttons and actions, which are global
watch: { 'siteSettings',
isEditing(newValue, oldValue) { 'eventBus',
if (newValue) { 'isEditing'
this.enableEditMode(); ],
} else {
this.disableEditMode();
}
}
},
methods: { methods: {
getZoomForDisplay(axis) { getZoomForDisplay(axis) {
// zoom is internally handled logarithmically, because we want to have x0.5, x1, x2, x4 ... magnifications // 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. // 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()}%` return `${(Math.pow(2, this.zoom[axis]) * 100).toFixed()}%`
}, },
toggleZoomAr() { toggleZoomAr() {
@ -267,7 +125,8 @@ export default {
// this.eventBus.send('set-zoom', {zoom: 1, axis: 'y'}); // this.eventBus.send('set-zoom', {zoom: 1, axis: 'y'});
// this.eventBus.send('set-zoom', {zoom: 1, axis: 'x'}); // this.eventBus.send('set-zoom', {zoom: 1, axis: 'x'});
this.eventBus?.sendToTunnel('set-zoom', {zoom: 1}); this.eventBus?.sendToTunnel('set-zoom', {zoom: 1, axis: 'y'});
this.eventBus?.sendToTunnel('set-zoom', {zoom: 1, axis: 'x'});
}, },
changeZoom(newZoom, axis) { changeZoom(newZoom, axis) {
// we store zoom logarithmically on this compnent // we store zoom logarithmically on this compnent
@ -281,33 +140,17 @@ export default {
newZoom = Math.pow(2, newZoom); newZoom = Math.pow(2, newZoom);
if (this.zoomAspectRatioLocked) { if (this.zoomAspectRatioLocked) {
this.eventBus?.sendToTunnel('set-zoom', {zoom: newZoom}); this.eventBus?.sendToTunnel('set-zoom', {zoom: newZoom, axis: 'y'});
this.eventBus?.sendToTunnel('set-zoom', {zoom: newZoom, axis: 'x'});
} else { } else {
this.eventBus?.sendToTunnel('set-zoom', {zoom: {[axis ?? 'x']: newZoom}}); this.eventBus?.sendToTunnel('set-zoom', {zoom: newZoom, axis: axis ?? 'x'});
} }
}, },
isActiveZoom(command) {
return false;
}
} }
} }
</script> </script>
<style lang="scss" src="../../../../res/css/flex.scss" scoped></style> <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="../../../res-common/panels.scss" scoped></style>
<style lang="scss" src="@csui/src/res-common/common.scss" scoped></style> <style lang="scss" 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

@ -1,433 +1,351 @@
<template> <template>
<div class="flex flex-col"> <div class="flex flex-col tab-root">
<div class="flex flex-row"> <!-- ADD ANY OPTION BARS HERE -->
<h1>Video player options</h1>
</div>
<div class="flex flex-row">
<div style="width: 48%">
NEW PLAYER SELECTOR
<div class="sub-panel-content">
<p>
You're probably on this page because Ultrawidify doesn't crop the player correctly.
</p>
<p>
If you hover over the boxes below, the corresponding element will be highlighted with golden outline. Player should be the first element that covers the video player on the page.
</p>
<p>
You need to reload the page for changes to take effect.
</p>
<!-- <p> <!-- The rest of the tab -->
<a @click="showAdvancedOptions = !showAdvancedOptions"> <div class="flex flex-row flex-wrap">
<template v-if="showAdvancedOptions">Hide advanced options</template>
<template v-else>Show advanced options</template>
</a>
</p> -->
<div v-if="showAdvancedOptions" style="display: flex; flex-direction: row"> <!-- Player element picker -->
<div style="display: flex; flex-direction: column"> <div class="sub-panel">
<div> <div class="flex flex-row">
<input :checked="playerManualQs" <h1><mdicon name="television-play" :size="32" /> Player element</h1>
@change="togglePlayerManualQs" </div>
type="checkbox" <div class="flex flex-row">
/> <div class="sub-panel-content">
<p>
You're probably on this page because Ultrawidify doesn't crop the player correctly.
</p>
<p>
If you hover over the boxes below, the corresponding element will change (sepia filter + higher brightness + reduced contrast + it gets an outline). Player element
should be the closest element to the video element, for which the sepia/brightness effect covers the area you expect the video will cover.
</p>
<p>
You need to reload the page for changes to take effect.
</p>
<!-- <p>
<a @click="showAdvancedOptions = !showAdvancedOptions">
<template v-if="showAdvancedOptions">Hide advanced options</template>
<template v-else>Show advanced options</template>
</a>
</p> -->
<div v-if="showAdvancedOptions" style="display: flex; flex-direction: row">
<div style="display: flex; flex-direction: column">
<div> <div>
Use <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors" target="_blank">CSS selector</a> for player<br/> <input :checked="playerManualQs"
<small>If defining multiple selectors, separate them with commas.</small> @change="togglePlayerManualQs"
type="checkbox"
/>
<div>
Use <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors" target="_blank">CSS selector</a> for player<br/>
<small>If defining multiple selectors, separate them with commas.</small>
</div>
</div> </div>
<div>Selector</div>
<input type="text"
v-model="playerQs"
@change="updatePlayerQuerySelector"
@blur="updatePlayerQuerySelector"
:disabled="playerByNodeIndex || !playerManualQs"
/>
</div>
<div style="display: flex; flex-direction: column">
<b>Custom CSS for site</b>
<textarea></textarea>
</div> </div>
<div>Selector</div>
<input type="text"
v-model="playerQs"
@change="updatePlayerQuerySelector"
@blur="updatePlayerQuerySelector"
:disabled="playerByNodeIndex || !playerManualQs"
/>
</div> </div>
<div style="display: flex; flex-direction: column">
<b>Custom CSS for site</b>
<textarea></textarea>
</div>
</div>
<div style="display: flex; flex-direction: row;"> <div style="display: flex; flex-direction: row;">
<div class="element-tree"> <div class="element-tree">
<table> <table>
<thead> <thead>
<tr> <tr>
<th> <th>
<div class="status-relative"> <div class="status-relative">
Status <mdicon name="help-circle" @click="showLegend = !showLegend" /> Status <mdicon name="help-circle" @click="showLegend = !showLegend" />
<div v-if="showLegend" class="element-symbol-legend"> <div v-if="showLegend" class="element-symbol-legend">
<b>Symbols:</b><br /> <b>Symbols:</b><br />
<mdicon name="alert-remove" class="invalid" /> Element of invalid dimensions<br /> <mdicon name="alert-remove" class="invalid" /> Element of invalid dimensions<br />
<mdicon name="refresh-auto" class="auto-match" /> Ultrawidify's player detection thinks this should be the player<br /> <mdicon name="refresh-auto" class="auto-match" /> Ultrawidify's player detection thinks this should be the player<br />
<mdicon name="bookmark" class="parent-offset-match" /> Site settings say this should be the player (based on counting parents)<br /> <mdicon name="bookmark" class="parent-offset-match" /> Site settings say this should be the player (based on counting parents)<br />
<mdicon name="crosshairs" class="qs-match" /> Site settings say this should be the player (based on query selectors)<br /> <mdicon name="crosshairs" class="qs-match" /> Site settings say this should be the player (based on query selectors)<br />
<mdicon name="check-circle" class="activePlayer" /> Element that is actually the player <mdicon name="check-circle" class="activePlayer" /> Element that is actually the player
</div>
</div>
</th>
<th>Element</th>
<!-- <th>Actions</th> -->
<!-- <th>Quick fixes</th> -->
</tr>
</thead>
<tbody>
<tr
v-for="(element, index) of elementStack"
:key="index"
class="element-row"
>
<td>
<div class="status">
<div
v-if="element.heuristics?.invalidSize"
class="invalid"
>
<mdicon name="alert-remove" />
</div>
<div
v-if="element.heuristics?.autoMatch"
class="auto-match"
>
<mdicon name="refresh-auto" />
</div>
<div
v-if="element.heuristics?.qsMatch"
class="qs-match"
>
<mdicon name="crosshairs" />
</div>
<div
v-if="element.heuristics?.manualElementByParentIndex"
class="parent-offset-match"
>
<mdicon name="bookmark" />
</div>
<div
v-if="element.heuristics?.activePlayer"
class="activePlayer"
>
<mdicon name="check-circle" />
</div>
</div>
</td>
<td>
<div
class="element-data"
@mouseover="markElement(index, true)"
@mouseleave="markElement(index, false)"
@click="setPlayer(index)"
>
<div class="tag">
<b>{{element.tagName}}</b> <i class="id">{{element.id ? `#`:''}}{{element.id}}</i> @ <span class="dimensions">{{element.width}}</span>x<span class="dimensions">{{element.height}}</span>
</div>
<div v-if="element.classList" class="class-list">
<div v-for="cls of element.classList" :key="cls">
{{cls}}
</div> </div>
</div> </div>
</div> </th>
</td> <th>Element</th>
<td> <!-- <th>Actions</th> -->
<div class="flex flex-row"> <!-- <th>Quick fixes</th> -->
<!-- <div @click="designatePlayer(index)">Set as player {{ index }}</div> --> </tr>
</div> </thead>
</td> <tbody>
<!-- <td> <tr
<div v-for="(element, index) of elementStack"
class="css-fixes" :key="index"
> class="element-row"
<div style="width: 100%"><b>Quick fixes:</b></div> >
<td>
<div class="status">
<div
v-if="element.heuristics?.invalidSize"
class="invalid"
>
<mdicon name="alert-remove" />
</div>
<div
v-if="element.heuristics?.autoMatch"
class="auto-match"
>
<mdicon name="refresh-auto" />
</div>
<div
v-if="element.heuristics?.qsMatch"
class="qs-match"
>
<mdicon name="crosshairs" />
</div>
<div
v-if="element.heuristics?.manualElementByParentIndex"
class="parent-offset-match"
>
<mdicon name="bookmark" />
</div>
<div
v-if="element.heuristics?.activePlayer"
class="activePlayer"
>
<mdicon name="check-circle" />
</div>
</div>
</td>
<td>
<div <div
class="css-line" class="element-data"
:class="{'active': cssStack[index]?.includes('width: 100%;')}"
@click="toggleCssForElement(index, 'width: 100%;')"
>
Width: 100%
</div>
<div
class="css-line"
:class="{'active': cssStack[index]?.includes('height: 100%;')}"
@click="toggleCssForElement(index, 'height: 100%;')"
>
Height: 100%
</div>
<div
class="css-line"
:class="{'active': cssStack[index]?.includes('display: flex;')}"
@click="toggleCssForElement(index, 'display: flex;')"
>
Display: flex
</div>
<div class="css-line">
Flex direction:
<span
class="css-line-suboption"
:class="{'active': cssStack[index]?.includes('flex-direction: row;')}"
@click="toggleCssForElement(index, 'flex-direction', 'row')"
>
row
</span> |
<span
class="css-line-suboption"
:class="{'active': cssStack[index]?.includes('flex-direction: column;')}"
@click="toggleCssForElement(index, 'flex-direction', 'column')"
>
column
</span>
</div>
<div class="css-line">
Justify content:
<span
class="css-line-suboption"
:class="{'active': cssStack[index]?.includes('justify-content: start;')}"
@click="toggleCssForElement(index, 'justify-content', 'start')"
>
start
</span> |
<span
class="css-line-suboption"
:class="{'active': cssStack[index]?.includes('justify-content: center;')}"
@click="toggleCssForElement(index, 'justify-content', 'center')"
>
center
</span> |
<span
class="css-line-suboption"
:class="{'active': cssStack[index]?.includes('justify-content: end;')}"
@click="toggleCssForElement(index, 'justify-content', 'end')"
>
end
</span>
</div>
<div class="css-line">
Align items:
<span
class="css-line-suboption"
:class="{'active': cssStack[index]?.includes('align-items: start;')}"
@click="toggleCssForElement(index, 'align-items', 'start')"
>
start
</span> |
<span
class="css-line-suboption"
:class="{'active': cssStack[index]?.includes('align-items: center;')}"
@click="toggleCssForElement(index, 'align-items', 'center')"
>
center
</span> |
<span
class="css-line-suboption"
:class="{'active': cssStack[index]?.includes('align-items: end;')}"
@click="toggleCssForElement(index, 'align-items', 'end')"
>
end
</span>
</div>
<div class="css-line"> @mouseover="markElement(index, true)"
Justify self: @mouseleave="markElement(index, false)"
<span
class="css-line-suboption" @click="setPlayer(index)"
:class="{'active': cssStack[index]?.includes('justify-self: start;')}" >
@click="toggleCssForElement(index, 'justify-self', 'start')" <div class="tag">
> <b>{{element.tagName}}</b> <i class="id">{{element.id ? `#`:''}}{{element.id}}</i> @ <span class="dimensions">{{element.width}}</span>x<span class="dimensions">{{element.height}}</span>
start
</span> | </div>
<span <div v-if="element.classList" class="class-list">
class="css-line-suboption" <div v-for="cls of element.classList" :key="cls">
:class="{'active': cssStack[index]?.includes('justify-self: center;')}" {{cls}}
@click="toggleCssForElement(index, 'justify-self', 'center')" </div>
> </div>
center
</span> |
<span
class="css-line-suboption"
:class="{'active': cssStack[index]?.includes('justify-self: end;')}"
@click="toggleCssForElement(index, 'justify-self', 'end')"
>
end
</span>
</div> </div>
<div class="css-line"> </td>
Align self: <td>
<span <div class="flex flex-row">
class="css-line-suboption" <!-- <div @click="designatePlayer(index)">Set as player {{ index }}</div> -->
:class="{'active': cssStack[index]?.includes('align-self: start;')}"
@click="toggleCssForElement(index, 'align-self', 'start')"
>
start
</span> |
<span
class="css-line-suboption"
:class="{'active': cssStack[index]?.includes('align-self: center;')}"
@click="toggleCssForElement(index, 'align-self', 'center')"
>
center
</span> |
<span
class="css-line-suboption"
:class="{'active': cssStack[index]?.includes('align-self: end;')}"
@click="toggleCssForElement(index, 'align-self', 'end')"
>
end
</span>
</div> </div>
<div class="css-line"> </td>
Text-align: <!-- <td>
<span <div
class="css-line-suboption" class="css-fixes"
:class="{'active': cssStack[index]?.includes('text-align: left;')}" >
@click="toggleCssForElement(index, 'text-align', 'left')" <div style="width: 100%"><b>Quick fixes:</b></div>
<div
class="css-line"
:class="{'active': cssStack[index]?.includes('width: 100%;')}"
@click="toggleCssForElement(index, 'width: 100%;')"
> >
left Width: 100%
</span> | </div>
<span <div
class="css-line-suboption" class="css-line"
:class="{'active': cssStack[index]?.includes('text-align: center;')}" :class="{'active': cssStack[index]?.includes('height: 100%;')}"
@click="toggleCssForElement(index, 'text-align', 'center')" @click="toggleCssForElement(index, 'height: 100%;')"
> >
center Height: 100%
</span> | </div>
<span <div
class="css-line-suboption" class="css-line"
:class="{'active': cssStack[index]?.find(x => x.includes('text-align: right'))}" :class="{'active': cssStack[index]?.includes('display: flex;')}"
@click="toggleCssForElement(index, 'text-align', 'right')" @click="toggleCssForElement(index, 'display: flex;')"
> >
right Display: flex
</span> </div>
<div class="css-line">
Flex direction:
<span
class="css-line-suboption"
:class="{'active': cssStack[index]?.includes('flex-direction: row;')}"
@click="toggleCssForElement(index, 'flex-direction', 'row')"
>
row
</span> |
<span
class="css-line-suboption"
:class="{'active': cssStack[index]?.includes('flex-direction: column;')}"
@click="toggleCssForElement(index, 'flex-direction', 'column')"
>
column
</span>
</div>
<div class="css-line">
Justify content:
<span
class="css-line-suboption"
:class="{'active': cssStack[index]?.includes('justify-content: start;')}"
@click="toggleCssForElement(index, 'justify-content', 'start')"
>
start
</span> |
<span
class="css-line-suboption"
:class="{'active': cssStack[index]?.includes('justify-content: center;')}"
@click="toggleCssForElement(index, 'justify-content', 'center')"
>
center
</span> |
<span
class="css-line-suboption"
:class="{'active': cssStack[index]?.includes('justify-content: end;')}"
@click="toggleCssForElement(index, 'justify-content', 'end')"
>
end
</span>
</div>
<div class="css-line">
Align items:
<span
class="css-line-suboption"
:class="{'active': cssStack[index]?.includes('align-items: start;')}"
@click="toggleCssForElement(index, 'align-items', 'start')"
>
start
</span> |
<span
class="css-line-suboption"
:class="{'active': cssStack[index]?.includes('align-items: center;')}"
@click="toggleCssForElement(index, 'align-items', 'center')"
>
center
</span> |
<span
class="css-line-suboption"
:class="{'active': cssStack[index]?.includes('align-items: end;')}"
@click="toggleCssForElement(index, 'align-items', 'end')"
>
end
</span>
</div>
<div class="css-line">
Justify self:
<span
class="css-line-suboption"
:class="{'active': cssStack[index]?.includes('justify-self: start;')}"
@click="toggleCssForElement(index, 'justify-self', 'start')"
>
start
</span> |
<span
class="css-line-suboption"
:class="{'active': cssStack[index]?.includes('justify-self: center;')}"
@click="toggleCssForElement(index, 'justify-self', 'center')"
>
center
</span> |
<span
class="css-line-suboption"
:class="{'active': cssStack[index]?.includes('justify-self: end;')}"
@click="toggleCssForElement(index, 'justify-self', 'end')"
>
end
</span>
</div>
<div class="css-line">
Align self:
<span
class="css-line-suboption"
:class="{'active': cssStack[index]?.includes('align-self: start;')}"
@click="toggleCssForElement(index, 'align-self', 'start')"
>
start
</span> |
<span
class="css-line-suboption"
:class="{'active': cssStack[index]?.includes('align-self: center;')}"
@click="toggleCssForElement(index, 'align-self', 'center')"
>
center
</span> |
<span
class="css-line-suboption"
:class="{'active': cssStack[index]?.includes('align-self: end;')}"
@click="toggleCssForElement(index, 'align-self', 'end')"
>
end
</span>
</div>
<div class="css-line">
Text-align:
<span
class="css-line-suboption"
:class="{'active': cssStack[index]?.includes('text-align: left;')}"
@click="toggleCssForElement(index, 'text-align', 'left')"
>
left
</span> |
<span
class="css-line-suboption"
:class="{'active': cssStack[index]?.includes('text-align: center;')}"
@click="toggleCssForElement(index, 'text-align', 'center')"
>
center
</span> |
<span
class="css-line-suboption"
:class="{'active': cssStack[index]?.find(x => x.includes('text-align: right'))}"
@click="toggleCssForElement(index, 'text-align', 'right')"
>
right
</span>
</div>
<div class="css-line">
Position:
<span
class="css-line-suboption"
:class="{'active': cssStack[index]?.includes('position: relative;')}"
@click="toggleCssForElement(index, 'position', 'relative')"
>
relative
</span> |
<span
class="css-line-suboption"
:class="{'active': cssStack[index]?.includes('position: absolute;')}"
@click="toggleCssForElement(index, 'position', 'absolute')"
>
absolute
</span>
</div>
</div> </div>
<div class="css-line"> </td> -->
Position: </tr>
<span </tbody>
class="css-line-suboption" </table>
:class="{'active': cssStack[index]?.includes('position: relative;')}" <div class="element-config">
@click="toggleCssForElement(index, 'position', 'relative')" </div>
>
relative
</span> |
<span
class="css-line-suboption"
:class="{'active': cssStack[index]?.includes('position: absolute;')}"
@click="toggleCssForElement(index, 'position', 'absolute')"
>
absolute
</span>
</div>
</div>
</td> -->
</tr>
</tbody>
</table>
<div class="element-config">
</div> </div>
</div>
<!-- <div class="css-preview"> <!-- <div class="css-preview">
{{cssStack}} {{cssStack}}
</div> --> </div> -->
</div>
</div> </div>
<!-- <div class="sub-panel-content">
<h2>Advanced settings</h2>
</div> -->
</div> </div>
</div> </div>
<div style="width: 48%">
<div>EXAMPLE <small style="opacity: 0.5"><br/>ps: i know this ui is shit, no need to tell me</small></div>
<div class="demo-images">
<div class="fig1">
<img src="/res/img/player-select-demo/uw_player_select___too_little.webp" />
<div>If you see this when hovering over the element, you need to select further down the list.</div>
</div>
<div class="fig2">
<div>
<p>
If you see this when hovering over the element, you're hovering over the correct element.
</p>
<p>
In case there's more than one element that covers the entire player and nothing more, select the option that's closest to the top of the list, otherwise in-player UI could break.
</p>
<p>
If in-player UI breaks, you can make the settings window appear from the extension popup.
</p>
</div>
<img src="/res/img/player-select-demo/uw_player_select___just-right.webp" />
</div>
<div class="fig1">
<img src="/res/img/player-select-demo/uw_player_select___too_much.webp" />
<div>If you see this when hovering over the element, you need to select an element closer to the top of the list.</div>
</div>
</div>
</div>
<div style="width: 48%" v-if="false">
<h2>Legacy advanced settings</h2>
<div class="">
<h3>Player element</h3>
<div class="field">
<div class="label">Manually specify player</div>
<input type="checkbox" />
</div>
<div class="field">
<div class="label">Select player by/as:</div>
<div class="select">
<select>
<option>n-th parent of video</option>
<option>Query selectors</option>
</select>
</div>
</div>
<div class="field">
<div class="label">Player is n-th parent of video</div>
<div class="range-input">
<input type="range" min="0" max="100" />
<input />
</div>
</div>
<div class="field">
<div class="label">Query selector for player element</div>
<div class="input">
<input />
</div>
</div>
<div class="field">
<div class="label">In full screen, calculate crop based on monitor resolution</div>
<input type="checkbox" />
</div>
<h3>Video element</h3>
<div class="field">
<div class="label">Select video element automatically</div>
<input type="checkbox">
</div>
<div class="field">
<div class="label">Query selectors</div>
<div class="input">
<input>
</div>
</div>
<div class="field">
<div class="label">Additional styles for video element</div>
<div class="input">
<textarea></textarea>
</div>
</div>
<h3>Additional css for this page</h3>
<div class="field">
<div class="label">Additional CSS for this page</div>
<textarea></textarea>
</div>
</div>
</div>
<!-- <div class="sub-panel-content">
<h2>Advanced settings</h2>
</div> -->
</div> </div>
</div> </div>
</template> </template>
@ -455,20 +373,11 @@ export default({
'isPopup' 'isPopup'
], ],
created() { created() {
this.eventBus.subscribe( this.eventBus.subscribe('uw-config-broadcast', {function: (config) => this.handleElementStack(config)});
'uw-config-broadcast',
{
source: this,
function: (config) => this.handleElementStack(config)
}
);
}, },
mounted() { mounted() {
this.getPlayerTree(); this.getPlayerTree();
}, },
destroyed() {
this.eventBus.unsubscribeAll(this);
},
computed: {}, computed: {},
methods: { methods: {
getPlayerTree() { getPlayerTree() {
@ -549,9 +458,6 @@ export default({
}) })
</script> </script>
<style lang="scss" src="../../res/css/flex.scss" scoped module></style>
<style lang="scss" src="@csui/src/res-common/panels.scss" scoped module></style>
<style lang="scss" src="@csui/src/res-common/common.scss" scoped module></style>
<style lang="scss" scoped> <style lang="scss" scoped>
p { p {
font-size: 1rem; font-size: 1rem;
@ -660,32 +566,4 @@ p {
.tab-root { .tab-root {
width: 100%; width: 100%;
} }
.demo-images {
width: 100%;
display: flex;
flex-direction: column;
padding-top: 2rem;
.fig1, .fig2 {
margin-top: -2rem;
max-height: 18rem;
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
}
.fig1 {
align-self: start;
}
.fig2 {
align-self: end;
}
img {
max-width: 32rem;
}
}
</style> </style>

View File

@ -1,296 +0,0 @@
<template>
<div class="flex flex-col" style="position: relative; width: 100%;">
<!-- The rest of the tab is under 'edit ratios and shortcuts' row -->
<div class="flex flex-col" style="width: 100%">
<h2>Player UI options</h2>
<div class="flex flex-col compact-form">
<div v-if="!siteSettings.data.enableUI.fullscreen">
UI is disabled for this site.
</div>
<div
class="flex flex-col field-group compact-form"
:class="{disabled: !siteSettings.data.enableUI.fullscreen}"
>
<div class="field disabled">
<div class="label">
Popup activator position:
</div>
<div class="select">
<select
v-model="settings.active.ui.inPlayer.popupAlignment"
@change="saveSettings()"
>
<option value="left">Left</option>
<option value="right">Right</option>
</select>
</div>
</div>
<div class="field">
<div class="label">
Activate in-player UI:
</div>
<div class="select">
<select
v-model="settings.active.ui.inPlayer.activation"
@change="saveSettings()"
>
<option value="player">
When mouse hovers over player
</option>
<option value="trigger-zone">
When mouse hovers over trigger zone
</option>
</select>
</div>
</div>
<div class="field" :class="{'disabled': settings.active.ui.inPlayer.activation !== 'trigger-zone'}">
<div class="label">Edit trigger zone:</div>
<button @click="startTriggerZoneEdit()">Edit</button>
</div>
<div class="field">
<div class="label">
Do not show in-player UI when video player is narrower than
</div>
<div class="input range-input">
<input
:value="settings.active.ui.inPlayer.minEnabledWidth"
class="slider"
type="range"
min="0"
max="1"
step="0.01"
@input="(event) => setPlayerRestrictions('minEnabledWidth', event.target.value)"
@change="(event) => saveSettings()"
>
<input
style="margin-right: 0.6rem;"
:value="ghettoComputed.minEnabledWidth"
@input="(event) => setPlayerRestrictions('minEnabledWidth', event.target.value, true)"
@change="(event) => saveSettings(true)"
>
<div class="unit">% of screen</div>
</div>
</div>
<div class="field">
<div class="label">
Do not show in-player UI when video player is shorter than
</div>
<div class="input range-input">
<input
:value="settings.active.ui.inPlayer.minEnabledHeight"
class="slider"
type="range"
min="0"
max="1"
step="0.01"
@input="(event) => setPlayerRestrictions('minEnabledHeight', event.target.value)"
@change="(event) => saveSettings()"
>
<input
style="margin-right: 0.6rem;"
:value="ghettoComputed.minEnabledHeight"
@input="(event) => setPlayerRestrictions('minEnabledHeight', event.target.value, true)"
@change="(event) => saveSettings(true)"
>
<div class="unit">% of screen</div>
</div>
</div>
</div>
</div>
<h2 class="mt2r">Menu options and keyboard shortcuts</h2>
<div>
Click 'add new' to add a new option. Click a button to edit or remove the keyboard shortcut.
</div>
<div class="keyboard-settings">
<!-- CROP OPTIONS -->
<div>
<div class="flex flex-row">
<h3 class="mth3">CROP OPTIONS</h3>
</div>
<CropOptionsPanel
:settings="settings"
:eventBus="eventBus"
:isEditing="true"
>
</CropOptionsPanel>
</div>
<!-- STRETCH OPTIONS -->
<div>
<div class="flex flex-row">
<h3 class="mth3">STRETCH OPTIONS</h3>
</div>
<StretchOptionsPanel
:settings="settings"
:eventBus="eventBus"
: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>
</div>
</template>
<script>
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,
ZoomOptionsPanel,
},
data() {
return {
ghettoComputed: { }
}
},
mixins: [
],
props: [
'settings', // required for buttons and actions, which are global
'siteSettings',
'eventBus',
],
mounted() {
this.ghettoComputed = {
minEnabledWidth: this.optionalToFixed(this.settings.active.ui.inPlayer.minEnabledWidth * 100, 0),
minEnabledHeight: this.optionalToFixed(this.settings.active.ui.inPlayer.minEnabledHeight * 100, 0),
}
},
methods: {
forcePositiveNumber(value) {
// Change EU format to US if needed
// | remove everything after second period if necessary
// | | | remove non-numeric characters
// | | | |
return value.replaceAll(',', '.').split('.', 2).join('.').replace(/[^0-9.]/g, '');
},
optionalToFixed(v, n) {
if ((`${v}`.split('.')[1]?.length ?? 0) > n) {
return v.toFixed(n);
}
return v;
},
setPlayerRestrictions(key, value, isTextInput) {
if (isTextInput) {
value = (+this.forcePositiveNumber(value) / 100);
}
if (isNaN(+value)) {
value = 0.5;
}
this.settings.active.ui.inPlayer[key] = value;
if (isTextInput) {
this.ghettoComputed[key] = this.optionalToFixed(value, 0);
} else {
this.ghettoComputed[key] = this.optionalToFixed(value * 100, 0);
}
},
saveSettings(forceRefresh) {
this.settings.saveWithoutReload();
if (forceRefresh) {
this.$nextTick( () => this.$forceRefresh() );
}
},
startTriggerZoneEdit() {
this.eventBus.send('start-trigger-zone-edit');
},
async openOptionsPage() {
BrowserDetect.runtime.openOptionsPage();
},
}
}
</script>
<style lang="scss" src="../../res/css/flex.scss" scoped module></style>
<style lang="scss" src="@csui/src/res-common/panels.scss" scoped module></style>
<style lang="scss" src="@csui/src/res-common/common.scss" scoped module></style>
<style lang="scss" scoped>
.justify-center {
justify-content: center;
}
.items-center {
align-items: center;
}
.mt-4{
margin-top: 1rem;
}
.input {
max-width: 24rem;
}
.trigger-zone-editor {
background-color: rgba(0,0,0,0.25);
padding-bottom: 2rem;
.field {
margin-bottom: -1em;
}
}
.disabled {
pointer-events: none;
/* color: #666; */
filter: contrast(50%) brightness(40%) grayscale(100%);
}
.compact-form {
> .field, > .field-group {
margin-top: 0;
margin-bottom: 0;
}
}
.keyboard-settings {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 1rem;
> * {
width: calc(33% - 0.5rem);
}
}
.mt2r {
margin-top: 2rem;
margin-bottom: 0.5rem;
}
.mth3 {
margin-top: 1.5rem;
}
</style>

View File

@ -1,30 +0,0 @@
<template>
<div class="flex flex-col">
<h1>Reset and backup</h1>
<p>
Pressing the button will reset settings to default without asking.
</p>
<button
class="danger"
@click="resetSettings"
>
Reset settings
</button>
</div>
</template>
<script>
export default {
props: {
settings: Object
},
methods: {
resetSettings() {
this.settings.active = JSON.parse(JSON.stringify(this.settings.default));
this.settings.saveWithoutReload();
}
}
}
</script>
<style lang="scss" src="../../res/css/flex.scss" scoped module></style>
<style lang="scss" src="@csui/src/res-common/panels.scss" scoped module></style>
<style lang="scss" src="@csui/src/res-common/common.scss" scoped module></style>

View File

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

View File

@ -38,7 +38,7 @@
<!-- CROP OPTIONS --> <!-- CROP OPTIONS -->
<div v-if="settings" class="sub-panel"> <div v-if="settings" class="sub-panel">
<div class="flex flex-row"> <div class="flex flex-row">
<mdicon name="crop" :size="16" /> <mdicon name="crop" :size="32" />
<h1>Crop video:</h1> <h1>Crop video:</h1>
</div> </div>
@ -66,6 +66,49 @@
></StretchOptionsPanel> ></StretchOptionsPanel>
</div> </div>
</div> </div>
<div class="flex flex-col">
<!-- VIDEO ALIGNMENT -->
<div class="sub-panel">
<div class="flex flex-row">
<mdicon name="align-horizontal-center" :size="32" />
<h1>Video alignment:</h1>
</div>
<div class="flex flex-row justify-center mt-4">
<alignment-options-control-component
:eventBus="eventBus"
>
</alignment-options-control-component>
</div>
<!-- <div class="flex flex-row flex-wrap">
<div class="m-t-0-33em display-block">
<input id="_input_zoom_site_allow_pan"
type="checkbox"
/>
Pan with mouse
</div>
</div> -->
</div>
<!-- ZOOM OPTIONS -->
<!-- <div class="sub-panel">
<div class="flex flex-row">
<mdicon name="magnify-plus-outline" :size="32" />
<h1>Manual zoom:</h1>
</div>
<ZoomOptionsPanel
:settings="settings"
:siteSettings="siteSettings"
:eventBus="eventBus"
:isEditing="editMode"
></ZoomOptionsPanel>
</div> -->
</div>
</div> </div>
</div> </div>
</template> </template>
@ -110,20 +153,11 @@ export default {
'site' 'site'
], ],
created() { created() {
this.eventBus.subscribe( this.eventBus.subscribe('uw-config-broadcast', {function: (config) => this.handleConfigBroadcast(config)});
'uw-config-broadcast',
{
source: this,
function: (config) => this.handleConfigBroadcast(config)
}
);
}, },
mounted() { mounted() {
this.eventBus.sendToTunnel('get-ar'); this.eventBus.sendToTunnel('get-ar');
}, },
destroyed() {
this.eventBus.unsubscribeAll(this);
},
components: { components: {
ShortcutButton, ShortcutButton,
EditShortcutButton, EditShortcutButton,
@ -143,9 +177,8 @@ export default {
</script> </script>
<style lang="scss" src="../../res/css/flex.scss" scoped module></style> <style lang="scss" src="../../res/css/flex.scss" scoped module></style>
<style lang="scss" src="@csui/src/res-common/panels.scss" scoped module></style> <style lang="scss" src="../res-common/panels.scss" scoped module></style>
<style lang="scss" src="@csui/src/res-common/common.scss" scoped module></style> <style lang="scss" src="../res-common/common.scss" scoped module></style>
<style lang="scss" scoped> <style lang="scss" scoped>
.justify-center { .justify-center {
justify-content: center; justify-content: center;

View File

@ -1,17 +0,0 @@
<template>
<div>
</div>
</template>
<script>
export default {
props: {
siteSettings: Object,
hasDrm: Boolean,
problems: Object,
}
}
</script>
<style lang="scss">
</style>

View File

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

View File

@ -1,108 +0,0 @@
<template>
<div v-if="popupVisible" class="popup">
<div class="popup-window">
<div
class="header"
:class="{
'danger': dialogType === 'danger',
'warning': dialogType === 'warning',
'info': dialogType === 'info',
'normal': dialogType === 'normal'
}"
>
{{ dialogText || 'Confirm action' }}
</div>
<div class="body">
{{ dialogText || 'Are you sure you want to do that?' }}
</div>
<div class="footer">
<button
class="button confirm"
:class="{
'danger': dialogType === 'danger',
'warning': dialogType === 'warning',
'info': dialogType === 'info',
'normal': dialogType === 'normal'
}"
@click="confirmAction"
>
{{ confirmText || 'Confirm' }}
</button>
<button class="button" @click="popupVisible = false">{{ cancelText || 'Cancel' }}</button>
</div>
</div>
</div>
<button
:class="[
{
'danger': dialogType === 'danger',
'warning': dialogType === 'warning',
},
btnClass
]"
@click="popupVisible = true"
>
<slot></slot>
</button>
</template>
<script>
export default {
data() {
return {
popupVisible: false,
}
},
props: [
'btnClass',
'dialogTitle',
'dialogText',
'confirmText',
'cancelText',
'dialogType'
],
methods: {
confirmAction() {
this.popupVisible = false;
this.$emit('onConfirmed');
}
}
}
</script>
<style lang="scss" scoped>
.popup {
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

@ -172,7 +172,7 @@ export default {
</script> </script>
<style lang="scss" src="@csui/src/res-common/common.scss" scoped></style> <style lang="scss" src="../res-common/common.scss" scoped></style>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "../res-common/variables"; @import "../res-common/variables";

View File

@ -1,119 +0,0 @@
<template>
<div class="context-container" @mouseleave="hideContextMenu()">
<GhettoContextMenuItem
class="activator uw-clickable"
:css="{
'expand-left': alignment === 'left',
'expand-right': alignment === 'right',
}"
@click="showContextMenu()"
@mouseenter="showContextMenu()"
>
<slot name="activator"></slot>
</GhettoContextMenuItem>
<div
v-if="contextMenuVisible"
class="context-menu uw-clickable"
:class="{
'menu-left': alignment === 'left',
'menu-right': alignment === 'right'
}"
@mouseleave="hideContextMenu()"
>
<slot></slot>
</div>
</div>
</template>
<script>
import GhettoContextMenuItem from './GhettoContextMenuItem.vue';
export default {
components: {
GhettoContextMenuItem,
},
props: {
alignment: String,
},
data() {
return {
contextMenuVisible: false,
contextMenuHideTimeout: undefined,
}
},
methods: {
showContextMenu() {
this.contextMenuVisible = true;
},
hideContextMenu() {
this.contextMenuHideTimeout = setTimeout( () => {
this.contextMenuVisible = false;
}, 50);
}
}
}
</script>
<style lang="scss" scoped>
.context-container {
position: relative;
}
.context-menu-wrapper {
position: relative;
}
.context-menu {
position: absolute;
display: flex;
flex-direction: column;
min-width: 5rem;
top: 50%;
transform: translateY(-50%);
}
.menu-left {
right: 100%;
}
.menu-right {
left: 100%;
}
</style>
<style lang="scss">
.activator {
position: relative;
padding: 1rem 1.6rem;
font-size: .95rem;
padding: 1rem 1.6rem;
background-color: rgba(0, 0, 0, 0.75);
backdrop-filter: blur(16px) saturate(120%);
white-space: nowrap;
&:hover {
background-color: rgba(255, 128, 64, 0.95);
}
&.expand-left {
padding-left: 2.2rem;
}
&.expand-right {
padding-right: 2.2rem;
}
&.expand-left::before,
&.expand-right::after {
position: absolute;
top: 50%;
transform: translateY(-50%);
font-size: 0.88rem;
}
&.expand-left::before {
content: '⮜';
left: 0.5rem;
}
&.expand-right::after {
content: '⮞';
right: 0.5rem;
}
}
</style>

View File

@ -1,46 +0,0 @@
<template>
<div
class="flex flex-col flex-center center-text item"
:class="{
'can-hover': !disableHover,
...(css ? css : {}),
}"
>
<slot></slot>
</div>
</template>
<script>
export default {
props: {
label: String,
shortcut: String,
childAlignment: String,
css: Object,
disableHover: Boolean,
}
}
</script>
<style lang="scss" scoped>
.item {
position: relative;
font-size: .95rem;
font-family: 'Overpass';
padding: 1rem 1.6rem;
background-color: rgba(0, 0, 0, 0.75);
backdrop-filter: blur(16px) saturate(120%);
white-space: nowrap;
&.can-hover:hover {
background-color: rgba(0,0,0,0.98);
border-bottom: 1px solid #fa6;
}
display: flex;
flex-direction: column;
}
</style>

View File

@ -1,26 +0,0 @@
<template>
<div>
<GhettoContextMenuItem>
<template v-if="label">
{{label}} {{shortcut ? `(${shortcut})` : ''}}
</template>
<template v-else>
<slot></slot>
</template>
</GhettoContextMenuItem>
</div>
</template>
<script>
import GhettoContextMenuItem from './GhettoContextMenuItem.vue';
export default {
components: {
GhettoContextMenuItem,
},
props: {
label: String,
shortcut: String,
}
}
</script>

View File

@ -1,120 +0,0 @@
<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

@ -0,0 +1,83 @@
<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

@ -0,0 +1,90 @@
<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

@ -0,0 +1,91 @@
<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

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

@ -1,105 +0,0 @@
<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,148 +0,0 @@
<template>
<div v-if="siteSupportLevel === 'official'" class="site-support official" :style="supportLevelStyle">
<mdicon name="check-decagram" />
<div v-if="!small">Verified</div>
<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" :style="supportLevelStyle">
<mdicon name="account-group" />
<div v-if="!small">Community</div>
<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" :style="supportLevelStyle">
<mdicon name="help-circle-outline" />
<div v-if="!small">Unknown</div>
<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" :style="supportLevelStyle">
<mdicon name="account" />
<div v-if="!small">Modified by you</div>
<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" :style="supportLevelStyle">
<mdicon class="site-support no-support" name="checkbox-marked-circle" />
<div v-if="!small">Not supported</div>
<div class="tooltip" :style="tooltipStyle">
<template v-if="small">Not supported&nbsp;&nbsp;</template>
Extension is known to not work with this site.
</div>
</div>
</template>
<script>
export default {
props: {
siteSupportLevel: String,
small: Boolean,
supportLevelStyle: String,
tooltipStyle: String,
}
}
</script>
<style lang="scss" scoped>
.site-support {
display: inline-flex;
flex-direction: row;
align-items: center;
margin-left: 1rem;
border-radius: 8px;
padding: 0rem 1.5rem 0rem 1rem;
position: relative;
.tooltip {
padding: 1rem;
display: none;
position: absolute;
bottom: 0;
transform: translateY(110%);
width: 42em;
background-color: rgba(0,0,0,0.90);
color: #ccc;
z-index: 99999 !important;
white-space: normal;
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-word;
}
&:hover {
.tooltip {
display: block;
}
}
.mdi {
margin-right: 1rem;
}
&.official {
background-color: #fa6;
color: #000;
.mdi {
fill: #000 !important;
}
}
&.community {
background-color: rgb(85, 85, 179);
color: #fff;
.mdi {
fill: #fff !important;
}
}
&.officially-disabled {
background-color: rgb(132, 24, 40);
color: #eee;
.mdi {
fill: #eee !important;
}
}
&.no-support {
background-color: rgb(83, 76, 77);
color: #eee;
.mdi {
fill: #eee !important;
}
}
&.user-added {
border: 1px solid #ff0;
color: #ff0;
.mdi {
fill: #ff0 !important;
}
}
}
</style>

View File

@ -1,471 +0,0 @@
<template>
<!-- Preview + editor -->
<div
v-if="settings?.active?.ui"
class="active-trigger-area uw-clickable"
:style="triggerZoneStyles"
>
<div class="trigger-zone-editor"
@mousedown="(event) => handleMouseDown('offset', event)"
>
<div
class="uw-clickable corner tl"
@mousedown.stop="(event) => handleMouseDown('tl', event)"
>
</div>
<div
class="uw-clickable corner tr"
@mousedown.stop="(event) => handleMouseDown('tr', event)"
>
</div>
<div
class="uw-clickable corner bl"
@mousedown.stop="(event) => handleMouseDown('bl', event)"
>
</div>
<div
class="uw-clickable corner br"
@mousedown.stop="(event) => handleMouseDown('br', event)"
>
</div>
</div>
</div>
<!-- Sliders -->
<div
class="trigger-zone-editor-sliders-container"
>
<div class="panel uw-clickable">
<div class="trigger-zone-editor-window">
<div class="heading">
<h2>Trigger zone editor</h2>
</div>
<div>
<p>
Trigger zone is represented by this very obvious gold grid. Ultrawidify menu button will only show when mouse enters the area marked by the grid.
</p>
<p>
Note that interacting with the grid area is slightly broken. The distance dragged doesn't correspond with the amount of resizing. I don't plan on
fixing that because the amount of effort required to get it working perfectly doesn't correspond with the amount of utility this part of the UI will
see. Like, it works well enough and I want to enjoy some of my end-of-year PTO.
</p>
<p>
Sliders work as they should.
</p>
</div>
<div class="field">
<div class="label">Trigger zone width:</div>
<div class="input range-input">
<input
:value="settings.active.ui.inPlayer.triggerZoneDimensions.width"
class="slider"
type="range"
min="0.1"
max="1"
step="0.01"
@input="(event) => setValue('width', event.target.value)"
@change="(event) => updateSettings(true)"
>
<input
:value="ghettoComputed.width"
@input="(event) => setValue('width', event.target.value, true)"
@change="(event) => updateSettings(true)"
>
</div>
</div>
<div class="hint">
Width of the trigger zone (% of player area).
</div>
<div class="field">
<div class="label">Trigger zone height:</div>
<div class="input range-input">
<input
:value="settings.active.ui.inPlayer.triggerZoneDimensions.height"
type="range"
min="0.1"
max="1"
step="0.01"
@input="(event) => setValue('height', event.target.value)"
@change="(event) => updateSettings(true)"
>
<input
:value="ghettoComputed.height"
@input="(event) => setValue('height', event.target.value, true)"
@change="(event) => updateSettings(true)"
>
</div>
</div>
<div class="hint">
Height of the trigger zone (% of player area).
</div>
<div class="field">
<div class="label">Trigger zone horizontal offset:</div>
<div class="input range-input">
<input
v-model="settings.active.ui.inPlayer.triggerZoneDimensions.offsetX"
type="range"
min="-100"
max="100"
@input="(event) => setValue('offsetX', event.target.value)"
@change="(event) => updateSettings(true)"
>
<input
:value="settings.active.ui.inPlayer.triggerZoneDimensions.offsetX"
@input="(event) => setValue('offsetX', event.target.value)"
@change="(event) => updateSettings(true)"
>
</div>
</div>
<div class="hint">
By default, trigger zone is centered around the button. This option moves trigger zone left and right.
</div>
<div class="field">
<div class="label">Trigger zone vertical offset:</div>
<div class="input range-input">
<input
v-model="settings.active.ui.inPlayer.triggerZoneDimensions.offsetY"
type="range"
min="-100"
max="100"
@input="(event) => setValue('offsetY', event.target.value)"
@change="(event) => updateSettings(true)"
>
<input
:value="settings.active.ui.inPlayer.triggerZoneDimensions.offsetY"
@input="(event) => setValue('offsetY', event.target.value)"
@change="(event) => updateSettings(true)"
>
</div>
</div>
<div class="hint">
By default, trigger zone is centered around the button. This option moves trigger zone up and down.
</div>
<div class="action-row">
<button @click="finishTriggerZoneEdit">Finish editing</button>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
props: [
'settings',
'eventBus',
'playerDimensions',
],
watch: {
playerDimensions(newVal, oldVal) {
this.updateTriggerZones();
}
},
data() {
return {
triggerZoneStyles: {},
activeCornerDrag: undefined,
dragStartPosition: undefined,
dragStartConfiguration: undefined,
ghettoComputed: { }
}
},
created() {
document.addEventListener("mouseup", this.handleMouseUp);
document.addEventListener("mousemove", this.handleMouseMove);
this.ghettoComputed = {
width: this.optionalToFixed(this.settings.active.ui.inPlayer.triggerZoneDimensions.width * 100, 0),
height: this.optionalToFixed(this.settings.active.ui.inPlayer.triggerZoneDimensions.height * 100, 0)
};
this.updateTriggerZones(false);
},
methods: {
optionalToFixed(v, n) {
try {
if ((`${v}`.split('.')[1]?.length ?? 0) > n) {
return v.toFixed(n);
}
} catch (e) {
}
return v;
},
updateTriggerZones(forceRefresh = true) {
if (this.playerDimensions && this.settings?.active?.ui?.inPlayer?.triggerZoneDimensions) {
this.triggerZoneStyles = {
width: `${Math.round(this.playerDimensions.width * this.settings.active.ui.inPlayer.triggerZoneDimensions.width)}px`,
height: `${Math.round(this.playerDimensions.height * this.settings.active.ui.inPlayer.triggerZoneDimensions.height)}px`,
transform: `translate(${(this.settings.active.ui.inPlayer.triggerZoneDimensions.offsetX)}%, ${this.settings.active.ui.inPlayer.triggerZoneDimensions.offsetY}%)`,
};
}
// if (forceRefresh) {
// this.$forceUpdate();
// }
},
handleMouseDown(corner, event) {
this.activeCornerDrag = corner;
// we need to save this because we don't know the location of the player element,
// just its dimensions ... that means we need to
this.dragStartPosition = {
x: event.clientX,
y: event.clientY
};
this.dragStartConfiguration = JSON.parse(JSON.stringify(this.settings.active.ui.inPlayer.triggerZoneDimensions));
},
handleMouseUp(event) {
if (!this.activeCornerDrag) {
return;
}
this.activeCornerDrag = undefined;
this.settings.saveWithoutReload();
},
handleMouseMove(event) {
if (!this.activeCornerDrag) {
return;
}
if (this.activeCornerDrag === 'offset') {
this.handleMove(event);
} else {
this.handleResize(event);
}
this.updateTriggerZones();
},
handleResize(event) {
// drag distance in px
const dx = event.clientX - this.dragStartPosition.x;
const dy = event.clientY - this.dragStartPosition.y;
// convert drag distance to % of current width:
const dxr = dx / this.playerDimensions.width * 2;
const dyr = dy / this.playerDimensions.height * 2;
// // update settings:
let nw, nh;
switch (this.activeCornerDrag) {
case 'tl':
nw = this.dragStartConfiguration.width - dxr;
nh = this.dragStartConfiguration.height - dyr;
break;
case 'tr':
nw = this.dragStartConfiguration.width + dxr;
nh = this.dragStartConfiguration.height - dyr;
break;
case 'bl':
nw = this.dragStartConfiguration.width - dxr;
nh = this.dragStartConfiguration.height + dyr;
break;
case 'br':
nw = this.dragStartConfiguration.width + dxr;
nh = this.dragStartConfiguration.height + dyr;
break;
}
// ensure everything is properly limited
const cw = Math.min(1, Math.max(0.125, nw));
const ch = Math.min(1, Math.max(0.125, nh));
// // update properties
this.settings.active.ui.inPlayer.triggerZoneDimensions.width = cw;
this.settings.active.ui.inPlayer.triggerZoneDimensions.height = ch;
},
handleMove(event) {
const dx = event.clientX - this.dragStartPosition.x;
const dy = event.clientY - this.dragStartPosition.y;
// convert drag distance to % of current width:
const dxr = dx / this.playerDimensions.width;
const dyr = dy / this.playerDimensions.height;
// const [min, max] = this.settings.active.ui.inPlayer.popupAlignment === 'right' ? [5, 90] : [-90, -5];
// const [minCrossAxis, maxCrossAxis] = [-90, 90];
const min = -90;
const max = -5;
const minCrossAxis = -90;
const maxCrossAxis = 90;
const cx = Math.min(max, Math.max(min, this.settings.active.ui.inPlayer.triggerZoneDimensions.offsetX + dxr));
const cy = Math.min(maxCrossAxis, Math.max(minCrossAxis, this.settings.active.ui.inPlayer.triggerZoneDimensions.offsetY + dyr));
this.settings.active.ui.inPlayer.triggerZoneDimensions.offsetX = cx;
this.settings.active.ui.inPlayer.triggerZoneDimensions.offsetY = cy;
},
//#region slider window
forceNumber(value) {
// Change EU format to US if needed
// | remove everything after second period if necessary
// | | | remove non-numeric characters
// | | | |
return value.replaceAll(',', '.').split('.', 2).join('.').replace(/[^0-9.\-]/g, '');
},
setValue(key, originalValue, isTextInput) {
let value = originalValue;
if (isTextInput) {
value = (+this.forceNumber(value) / 100);
} else {
value = +this.forceNumber(value);
}
if (isNaN(+value)) {
value = 0.5;
}
this.settings.active.ui.inPlayer.triggerZoneDimensions[key] = value;
if (isTextInput) {
this.ghettoComputed[key] = this.optionalToFixed(originalValue, 0);
} else {
this.ghettoComputed[key] = this.optionalToFixed(originalValue * 100, 0);
}
this.updateSettings();
},
updateSettings(forceRefresh) {
this.settings.saveWithoutReload();
this.updateTriggerZones();
if (forceRefresh) {
this.$nextTick( () => this.$forceUpdate() );
}
},
//#endregion
finishTriggerZoneEdit() {
this.eventBus.send('finish-trigger-zone-edit');
},
}
}
</script>
<style lang="scss" src="@csui/src/res-common/panels.scss" scoped module></style>
<style lang="scss" src="@csui/src/res-common/common.scss" scoped module></style>
<style lang="scss" scoped>
.active-trigger-area {
background-image: url('/res/img/grid_512.webp');
background-position: center;
background-attachment: fixed;
}
.trigger-zone-editor {
width: 100%;
height: 100%;
position: relative;
> .corner {
position: absolute;
width: 32px;
height: 32px;
background-color: #000;
background-image: url('/res/img/corner-marker_64.webp');
background-size: cover;
}
.tr, .tl {
top: 0;
}
.br, .bl {
bottom: 0;
}
.tl, .bl {
left: 0;
}
.tr, .br {
right: 0;
}
.br {
transform: rotate(90deg);
}
.bl {
transform: rotate(180deg);
}
.tl {
transform: rotate(-90deg);
}
}
.trigger-zone-editor-window {
max-width: 69rem;
}
.trigger-zone-editor-sliders-container {
position: fixed;
width: 100dvw;
height: 100dvh;
top: 0;
left: 0;
z-index: 2000;
pointer-events: none;
display: flex;
align-items: center;
justify-content: center;
.panel {
backdrop-filter: blur(0.5rem) brightness(0.5);
color: #ccc;
pointer-events: all;
padding: 1rem;
}
.range-input {
display: flex;
flex-direction: row;
* {
margin-left: 0.5rem;
margin-right: 0.5rem;
}
input {
max-width: 5rem;
}
input[type=range] {
max-width: none;
}
}
.trigger-zone-editor {
padding-bottom: 2rem;
.field {
margin-bottom: -1em;
}
}
}
.range-input {
display: flex;
flex-direction: row;
* {
margin-left: 0.5rem;
margin-right: 0.5rem;
}
input {
max-width: 5rem;
}
input[type=range] {
max-width: none;
}
}
.action-row {
width: 100%;
display: flex;
flex-direction: row;
justify-content: flex-end;
}
</style>

View File

@ -1,77 +0,0 @@
<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

@ -7,20 +7,43 @@
<span class="label">Having an issue?</span><br/> Report <strike>undocumented features</strike> bugs using one of the following options (in order of preference): <span class="label">Having an issue?</span><br/> Report <strike>undocumented features</strike> bugs using one of the following options (in order of preference):
<ul> <ul>
<li> <a target="_blank" href="https://github.com/tamius-han/ultrawidify/issues"><b>Github (preferred)</b></a><br/></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">{{gmailLink}}</a></li> <li>Email: <a target="_blank" :href="mailtoLink">tamius.han@gmail.com</a></li>
</ul> </ul>
</div> </div>
<div class="flex-grow"></div>
<div class="row">
<span class="label">Swatter mode (logging)</span><br/>
</div>
<div v-if="showEasterEgg" class="center"><small>You've made plenty of marks, all in the wrong places!</small></div>
<div class="flex flex-row">
<ShortcutButton class="flex flex-grow button"
label="Show logger"
:active="loggingEnabled"
@click.native="showLogger()"
></ShortcutButton>
<ShortcutButton class="flex flex-grow button"
label="Make a mark"
@click.native="sendMark()"
></ShortcutButton>
<ShortcutButton class="flex flex-grow button"
label="Hide logger"
@click.native="hideLogger()"
></ShortcutButton>
</div>
</div> </div>
</template> </template>
<script> <script>
// import Comms from '@src/ext/lib/comms/Comms'; import Comms from '../../ext/lib/comms/Comms';
// import ShortcutButton from '@src/common/components/ShortcutButton'; import ShortcutButton from '../../common/components/ShortcutButton';
import BrowserDetect from '@src/ext/conf/BrowserDetect'; import BrowserDetect from '../../ext/conf/BrowserDetect';
export default { export default {
components: { components: {
}, ShortcutButton,
},
data() { data() {
return { return {
// reminder webextension-polyfill doesn't seem to work in vue! // reminder webextension-polyfill doesn't seem to work in vue!
@ -30,13 +53,11 @@ export default {
loggerSettingsError: false, loggerSettingsError: false,
lastLoadedLoggerSettings: undefined, lastLoadedLoggerSettings: undefined,
mailtoLink: '', mailtoLink: '',
redditLink: '',
showEasterEgg: false, showEasterEgg: false,
gmailLink: '',
} }
}, },
async created() { async created() {
const b64="dGFtaXVzLmhhbkBnbWFpbC5jb20";
this.gmailLink = atob(b64);
const messageTemplate = encodeURIComponent( const messageTemplate = encodeURIComponent(
`Describe your issue in more detail. In case of misaligned videos, please provide screenshots. When reporting\ `Describe your issue in more detail. In case of misaligned videos, please provide screenshots. When reporting\
issues with autodetection not detecting aspect ratio correctly, please provide a link with timestamp to the\ issues with autodetection not detecting aspect ratio correctly, please provide a link with timestamp to the\
@ -52,7 +73,7 @@ Browser-related stuff (please ensure this section is correct):
* Operating system: ${window.navigator.platform} * Operating system: ${window.navigator.platform}
` `
); );
this.mailtoLink = `mailto:${this.gmailLink}?subject=%5BUltrawidify%5D%20ENTER%20SUMMARY%20OF%20YOUR%20ISSUE%20HERE&body=${messageTemplate}`; this.mailtoLink = `mailto:tamius.han@gmail.com?subject=%5BUltrawidify%5D%20ENTER%20SUMMARY%20OF%20YOUR%20ISSUE%20HERE&body=${messageTemplate}`;
}, },
methods: { methods: {
async updateLoggerSettings(allowLogging) { async updateLoggerSettings(allowLogging) {
@ -70,15 +91,15 @@ Browser-related stuff (please ensure this section is correct):
// Logger.saveConfig({allowLogging: allowLogging, ...lastLoadedLoggerSettings}); // Logger.saveConfig({allowLogging: allowLogging, ...lastLoadedLoggerSettings});
// } // }
}, },
// showLogger() { showLogger() {
// Comms.sendMessage({cmd: 'show-logger', forwardToActive: true}); Comms.sendMessage({cmd: 'show-logger', forwardToActive: true});
// }, },
// sendMark() { sendMark() {
// this.showEasterEgg = !this.showEasterEgg; this.showEasterEgg = !this.showEasterEgg;
// }, },
// hideLogger() { hideLogger() {
// Comms.sendMessage({cmd: 'hide-logger', forwardToActive: true}); Comms.sendMessage({cmd: 'hide-logger', forwardToActive: true});
// } }
} }
} }
</script> </script>

View File

@ -1,207 +1,94 @@
<template> <template>
<div class="flex flex-col relative h-full" style="padding-bottom: 20px"> <div class="flex flex-col" style="padding-bottom: 20px">
<!-- <div class="flex flex-row">
Extension is disabled for a given site when it's disabled in full screen, since <mdicon name="crop" :size="24" />&nbsp;&nbsp;
current settings do not allow the extension to only be disabled while in full screen <h1>Crop video:</h1>
--> </div>
<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>
<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> <CropOptionsPanel
</template> style="margin-top: -2rem"
<template v-else> :settings="settings"
<div :eventBus="eventBus"
v-if="siteSettings.isEnabledForEnvironment(false, true) === ExtensionMode.Disabled" :siteSettings="siteSettings"
class="warning-compact" :isEditing="false"
> >
<div class="w-full flex flex-row"> </CropOptionsPanel>
<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"> <div class="flex flex-row">
<mdicon name="crop" :size="16" />&nbsp;&nbsp; <mdicon name="crop" :size="24" />&nbsp;&nbsp;
<span>CROP</span> <h1>Stretch video:</h1>
</div> </div>
<div
style="margin-top: -0.69rem; margin-bottom: 0.88rem;"
>
<CropOptionsPanel
:settings="settings"
:eventBus="eventBus"
:siteSettings="siteSettings"
:isEditing="false"
:compact="true"
>
</CropOptionsPanel>
</div>
<div class="flex flex-row"> <StretchOptionsPanel
<mdicon name="crop" :size="16" />&nbsp;&nbsp; style="margin-top: -2rem"
<span>STRETCH</span> :settings="settings"
</div> :eventBus="eventBus"
<div :siteSettings="siteSettings"
style="margin-top: -0.69rem; margin-bottom: 0.88rem;" :isEditing="false"
> ></StretchOptionsPanel>
<StretchOptionsPanel
:settings="settings"
:eventBus="eventBus"
:siteSettings="siteSettings"
:isEditing="false"
:compact="true"
></StretchOptionsPanel>
</div>
<div class="flex flex-row"> <div class="flex flex-row">
<mdicon name="crop" :size="16" />&nbsp;&nbsp; <mdicon name="crop" :size="24" />&nbsp;&nbsp;
<span>ZOOM</span> <h1>Zoom:</h1>
</div> </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"> <ZoomOptionsPanel
<mdicon name="crop" :size="16" />&nbsp;&nbsp; style="margin-top: -2rem"
<span>ALIGN</span> :settings="settings"
</div> :eventBus="eventBus"
<div :siteSettings="siteSettings"
style="margin-bottom: 0.88rem;" :isEditing="false"
>
</ZoomOptionsPanel>
<div class="flex flex-row">
<mdicon name="crop" :size="24" />&nbsp;&nbsp;
<h1>Video alignment:</h1>
</div>
<div class="flex flex-row">
<alignment-options-control-component
:eventBus="eventBus"
> >
<AlignmentOptionsControlComponent </alignment-options-control-component>
:eventBus="eventBus" </div>
:large="true"
> </AlignmentOptionsControlComponent>
</div>
</template>
</div> </div>
</template> </template>
<script> <script>
import CropOptionsPanel from '@csui/src/PlayerUiPanels/PanelComponents/VideoSettings/CropOptionsPanel'; import CropOptionsPanel from '../../PlayerUiPanels/PanelComponents/VideoSettings/CropOptionsPanel';
import StretchOptionsPanel from '@csui/src/PlayerUiPanels/PanelComponents/VideoSettings/StretchOptionsPanel.vue'; import StretchOptionsPanel from '../../PlayerUiPanels/PanelComponents/VideoSettings/StretchOptionsPanel.vue';
import ZoomOptionsPanel from '@csui/src/PlayerUiPanels/PanelComponents/VideoSettings/ZoomOptionsPanel.vue'; import ZoomOptionsPanel from '../../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 { export default {
components: { data() {
CropOptionsPanel, return {
StretchOptionsPanel, exec: null,
ZoomOptionsPanel, };
AlignmentOptionsControlComponent
}, },
mixins: [ mixins: [
], ],
props: [ props: [
'site',
'settings', 'settings',
'siteSettings', 'siteSettings',
'eventBus', 'eventBus',
'hosts'
], ],
data() { components: {
return { CropOptionsPanel, StretchOptionsPanel, ZoomOptionsPanel
exec: null,
ExtensionMode: ExtensionMode,
enabledHosts: [],
};
},
watch: {
hosts(val) {
this.filterActiveSites(val);
}
}, },
created() { created() {
this.eventBus.subscribe( this.eventBus.subscribe('uw-config-broadcast', {function: (config) => this.handleConfigBroadcast(config)});
'uw-config-broadcast',
{
source: this,
function: (config) => this.handleConfigBroadcast(config)
}
);
this.filterActiveSites(this.hosts);
}, },
mounted() { mounted() {
this.eventBus.sendToTunnel('get-ar'); this.eventBus.sendToTunnel('get-ar');
}, },
destroyed() {
this.eventBus.unsubscribeAll(this);
},
methods: { 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> </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

@ -117,9 +117,9 @@
</div> </div>
<div class="flex flex-col"> <div class="flex flex-col">
<textarea <textarea
v-model="playerCss" v-model="playerCss"
@change="updatePlayerCss" @change="updatePlayerCss"
@blur="updatePlayerCss" @blur="updatePlayerCss"
> >
</textarea> </textarea>
</div> </div>
@ -287,6 +287,7 @@ export default {
const observer = new IntersectionObserver( const observer = new IntersectionObserver(
([e]) => { ([e]) => {
// console.log('observer triggered. intersection ratio?', e.intersectionRatio)
saveButton.classList.toggle('floating', e.intersectionRatio < 0.95); saveButton.classList.toggle('floating', e.intersectionRatio < 0.95);
}, },
{threshold: [0, 0.5, 0.9, 0.95, 1]} {threshold: [0, 0.5, 0.9, 0.95, 1]}

View File

@ -0,0 +1,58 @@
<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,215 +0,0 @@
<template>
<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>
<template v-if="zoomAspectRatioLocked">
<div class="input range-input no-bg">
<input
type="range"
class="slider"
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>
<template v-else>
<div class="top-label">Horizontal zoom:</div>
<div class="input range-input no-bg">
<input
type="range"
class="slider"
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>
</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,
zoom: {
x: 0,
y: 0
},
// TODO: this should be mixin?
resizerConfig: {
crop: null,
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)
};
}
}
},
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.
return `${(Math.pow(2, this.zoom[axis]) * 100).toFixed()}%`
},
toggleZoomAr() {
this.zoomAspectRatioLocked = !this.zoomAspectRatioLocked;
},
resetZoom() {
// we store zoom logarithmically on this component
this.zoom = {x: 0, y: 0};
// we do not use logarithmic zoom elsewhere
// todo: replace eventBus with postMessage to parent
// 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});
},
changeZoom(newZoom, axis, isLinear) {
if (isNaN(+newZoom)) {
return;
}
let logZoom, linZoom;
if (isLinear) {
newZoom /= 100;
logZoom = Math.log2(newZoom);
linZoom = newZoom;
} else {
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
if (this.zoomAspectRatioLocked) {
this.eventBus?.sendToTunnel('set-zoom', {zoom: linZoom});
} else {
this.eventBus?.sendToTunnel('set-zoom', {zoom: {[axis ?? 'x']: linZoom}});
}
},
}
}
</script>
<style lang="scss" src="@csui/res/css/flex.scss" scoped></style>
<style lang="scss" src="@csui/src/res-common/panels.scss" scoped module></style>
<style lang="scss" src="@csui/src/res-common/common.scss" scoped module></style>

View File

@ -4,12 +4,6 @@ div, p, span {
font-size: 16px; font-size: 16px;
} }
.disabled {
pointer-events: none;
/* color: #666; */
filter: contrast(50%) brightness(40%) grayscale(100%);
}
.warning-box { .warning-box {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@ -36,7 +30,7 @@ h1, h2, h3 {
padding: 0; padding: 0;
} }
button, .button { .button {
background-color: rgba($blackBg, $normalTransparentOpacity); background-color: rgba($blackBg, $normalTransparentOpacity);
padding: 0.5rem 2rem; padding: 0.5rem 2rem;
@ -57,88 +51,16 @@ button, .button {
background-color: $primaryBg; background-color: $primaryBg;
border-color: rgba($primary, .5); border-color: rgba($primary, .5);
} }
&.primary {
background-color: #fa6;
color: #000;
}
&.danger {
background-color: #ff2211 !important;
color:#000;
}
&.disabled {
filter: saturate(0%);
}
} }
.b3 { .b3 {
margin: 0.25rem; margin: 0.25rem;
padding: 0.5rem 2rem; padding: 0.5rem 2rem;
} }
.input, .range-input {
background-color: rgba($blackBg, $normalTransparentOpacity);
border: 1px solid transparent;
border-bottom: 1px solid rgba(255,255,255,0.5);
padding-top: 0.25rem;
padding-bottom: 0.25rem;
position: relative;
&:active, &:focus, &:focus-within {
border-bottom: 1px solid rgba($primary, 0.5);
}
&.no-bg {
background-color: transparent;
border-color: transparent;
}
input {
width: 100%;
outline: none;
border: 1px solid transparent;
background-color: transparent;
color: #fff;
}
.unit {
position: absolute;
right: 0px;
pointer-events: none;
opacity: 0.69;
font-size: 0.8rem;
top: 0;
transform: translateY(69%);
}
}
.range-input {
display: flex;
flex-direction: row;
* {
margin-left: 0.5rem;
margin-right: 0.5rem;
}
input {
max-width: 5rem;
}
input[type=range] {
max-width: none;
}
}
.field { .field {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
box-sizing: border-box; flex-wrap: wrap;
width: 100%;
max-width: 100%;
margin-top: 0.5rem; margin-top: 0.5rem;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
@ -146,53 +68,44 @@ button, .button {
align-items: center; align-items: center;
&.l2 {
margin-left: 4rem;
}
.label { .label {
flex: 0 0 25%; flex: 0 0 25%;
min-width: 16rem;
text-align: right; text-align: right;
padding-right: 1rem; padding-right: 1rem;
}
.input {
flex: 0 0 70%;
background-color: rgba($blackBg, $normalTransparentOpacity);
border: 1px solid transparent;
border-bottom: 1px solid rgba(255,255,255,0.5);
padding-top: 0.25rem;
padding-bottom: 0.25rem;
.color-emphasis { &:active, &:focus, &:focus-within {
color: #fa6; border-bottom: 1px solid rgba($primary, 0.5);
} }
.sub-label {
font-size: 0.9em; input {
opacity: 0.69; width: 100%;
outline: none;
border: 1px solid transparent;
background-color: transparent;
color: #fff;
} }
} }
.hint {
.input, .range-input { padding-left: calc(25% + 1rem);
flex-grow: 1; font-size: 0.8rem;
flex-shrink: 1; opacity: 0.7;
max-width: 24rem; margin-top: 0.25rem;
min-width: 16rem; width: 100%;
} }
.has-hint {
display: flex;
flex-direction: column;
}
.select { .select {
flex-grow: 1;
flex-shrink: 1;
min-width: 12px;
max-width: 24rem;
select { select {
background: rgba($blackBg, $hoverTransparentOpacity); background: rgba($blackBg, $hoverTransparentOpacity);
min-width: 12px;
max-width: 100%;
color: #fff; color: #fff;
// border: 0px solid transparent; border: 0px solid transparent;
border: 1px solid rgba(255, 171, 102, 0.42);
padding: 0.5rem 1rem 0.25rem; padding: 0.5rem 1rem 0.25rem;
outline: none; outline: none;
@ -202,15 +115,6 @@ button, .button {
} }
} }
} }
.hint {
font-size: 0.8rem;
opacity: 0.7;
margin-top: 0.25rem;
margin-bottom: 0.75rem;
margin-left: 5rem;
// width: 100%;
box-sizing:border-box;
}
.options-bar { .options-bar {
position: absolute; position: absolute;
@ -231,15 +135,9 @@ button, .button {
padding-left: 0.33rem; padding-left: 0.33rem;
padding-right: 0.33rem; padding-right: 0.33rem;
} }
.b3-compact { .input-slider {
width: 7rem; width: 480px;
padding-left: 0.25rem;
padding-right: 0.25rem;
} }
// .input-slider {
// width: 480px;
// }
.warning-lite { .warning-lite {
padding-right: 16px; padding-right: 16px;
padding-bottom: 16px; padding-bottom: 16px;

View File

@ -5,55 +5,16 @@ export default {
* We can handle events with the same function we use to handle events from * We can handle events with the same function we use to handle events from
* the content script. * the content script.
*/ */
document.addEventListener('mousemove', (event) => { document.addEventListener('mousemove', (event) => {
this.handleProbe( this.handleProbe({
{ coords: {
coords: { x: event.clientX,
x: event.clientX, y: event.clientY
y: event.clientY }
}, }, this.origin);
isCompanion: true, });
},
this.origin
);
});
},
data() {
return {
playerDimensions: undefined,
triggerZoneStyles: {
height: '50dvh',
width: '50dvw',
transform: 'translateX(-50%)'
},
hoverStats: {
isOverTriggerZone: false,
isOverMenuTrigger: false,
isOverUIArea: false,
hasMouse: false,
}
}
}, },
methods: { methods: {
playerDimensionsUpdate(dimensions) {
if (!dimensions?.width || !dimensions?.height) {
this.playerDimensions = undefined;
}
if (dimensions?.width !== this.playerDimensions?.width || dimensions?.height !== this.playerDimensions?.height) {
this.playerDimensions = dimensions;
this.updateTriggerZones();
}
},
updateTriggerZones() {
if (this.playerDimensions && this.settings) {
this.triggerZoneStyles = {
width: `${Math.round(this.playerDimensions.width * this.settings.active.ui.inPlayer.triggerZoneDimensions.width)}px`,
height: `${Math.round(this.playerDimensions.height * this.settings.active.ui.inPlayer.triggerZoneDimensions.height)}px`,
transform: `translate(${(this.settings.active.ui.inPlayer.triggerZoneDimensions.offsetX)}%, ${this.settings.active.ui.inPlayer.triggerZoneDimensions.offsetY}%)`,
};
}
},
/** /**
* Handles 'uwui-probe' events. It checks whether there's a clickable element under * Handles 'uwui-probe' events. It checks whether there's a clickable element under
* cursor, and sends a reply to the content scripts that indicates whether pointer-events * cursor, and sends a reply to the content scripts that indicates whether pointer-events
@ -65,6 +26,16 @@ export default {
} }
this.lastProbeTs = eventData.ts; this.lastProbeTs = eventData.ts;
// show ultrawidify trigger zone and set it to vanish after 250ms
// but don't show the trigger zone behind an active popup
if (! this.uwWindowVisible) {
this.uwTriggerZoneVisible = true;
clearTimeout(this.uwTriggerZoneTimeout);
this.uwTriggerZoneTimeout = setTimeout(
() => this.uwTriggerZoneVisible = false,
250
);
}
/* we check if our mouse is hovering over an element. /* we check if our mouse is hovering over an element.
* *
@ -76,78 +47,22 @@ export default {
* our top-level element. * our top-level element.
*/ */
let isClickable = false; let isClickable = false;
let isOverTriggerZone = false; let element = document.elementFromPoint(eventData.coords.x, eventData.coords.y);
let isOverUIArea = false;
let isOverMenuTrigger = false;
const elements = document.elementsFromPoint(eventData.coords.x, eventData.coords.y);
if (!elements.length) { while (element) {
return; if (element?.classList.contains('uw-clickable')) {
} // we could set 'pointerEvents' here and now & simply use return, but that
// might cause us a problem if we ever try to add more shit to this function
for (const element of elements) {
if (element.classList?.contains('uw-clickable')) {
isClickable = true; isClickable = true;
break;
} }
if (element.classList?.contains('uw-ui-trigger')) { element = element.parentElement;
isOverTriggerZone = true;
}
if (element.classList?.contains('uw-ui-area')) {
isOverUIArea = true;
}
if (element.classList?.contains('uw-menu-trigger')) {
isOverMenuTrigger = true;
}
} }
this.triggerZoneActive = isOverTriggerZone;
// show ultrawidify trigger zone and set it to vanish after 250ms
// but don't show the trigger zone behind an active popup
if (
eventData.canShowUI
&& (this.settings.active.ui.inPlayer.activation === 'player' ? isOverUIArea : isOverTriggerZone)
) {
if (! this.uwWindowVisible) {
this.uwTriggerZoneVisible = true;
clearTimeout(this.uwTriggerZoneTimeout);
const windowParent = window.parent;
this.uwTriggerZoneTimeout = setTimeout(
() => {
this.uwTriggerZoneVisible = false;
windowParent.postMessage(
{action: 'uwui-hidden', opacity: false},
origin
);
},
500
);
}
} else {
// this.uwTriggerZoneVisible = false;
}
const hasMouse = !!document.querySelector(':hover');
this.hoverStats.isOverTriggerZone = isOverTriggerZone;
this.hoverStats.isOverMenuTrigger = isOverMenuTrigger;
this.hoverStats.isOverUIArea = isOverUIArea;
this.hoverStats.hasMouse = hasMouse
window.parent.postMessage( window.parent.postMessage(
{ {
action: 'uwui-clickable', action: 'uwui-clickable',
clickable: isClickable, clickable: isClickable,
opacity: isClickable || this.contextMenuActive || this.uwTriggerZoneVisible,
hoverStats: {
isOverTriggerZone: isOverTriggerZone,
isOverMenuTrigger: isOverMenuTrigger,
isOverUIArea: isOverUIArea,
hasMouse: hasMouse
},
ts: +new Date() ts: +new Date()
}, },
origin origin

View File

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

View File

@ -2,20 +2,13 @@ import Debug from './conf/Debug.js';
import BrowserDetect from './conf/BrowserDetect'; import BrowserDetect from './conf/BrowserDetect';
import CommsServer from './lib/comms/CommsServer'; import CommsServer from './lib/comms/CommsServer';
import Settings from './lib/Settings'; import Settings from './lib/Settings';
import Logger, { baseLoggingOptions } from './lib/Logger';
import { sleep } from '../common/js/utils'; import { sleep } from '../common/js/utils';
import EventBus, { EventBusCommand } from './lib/EventBus'; 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 { export default class UWServer {
settings: Settings; settings: Settings;
logger: ComponentLogger; logger: Logger;
logAggregator: LogAggregator;
comms: CommsServer; comms: CommsServer;
eventBus: EventBus; eventBus: EventBus;
@ -31,27 +24,27 @@ export default class UWServer {
} }
eventBusCommands = { eventBusCommands = {
'popup-set-selected-tab': { 'popup-set-selected-tab': [{
function: (message) => this.setSelectedTab(message.selectedMenu, message.selectedSubitem) function: (message) => this.setSelectedTab(message.selectedMenu, message.selectedSubitem)
}, }],
'has-video': { 'has-video': [{
function: (message, context) => this.registerVideo(context.comms.sender) function: (message, context) => this.registerVideo(context.comms.sender)
}, }],
'noVideo' : { 'noVideo' : [{
function: (message, context) => this.unregisterVideo(context.comms.sender) function: (message, context) => this.unregisterVideo(context.comms.sender)
}, }],
'inject-css': { 'inject-css': [{
function: (message, context) => this.injectCss(message.cssString, context.comms.sender) function: (message, context) => this.injectCss(message.cssString, context.comms.sender)
}, }],
'eject-css': { 'eject-css': [{
function: (message, context) => this.removeCss(message.cssString, context.comms.sender) function: (message, context) => this.removeCss(message.cssString, context.comms.sender)
}, }],
'replace-css': { 'replace-css': [{
function: (message, context) => this.replaceCss(message.oldCssString, message.newCssString, context.comms.sender) function: (message, context) => this.replaceCss(message.oldCssString, message.newCssString, context.comms.sender)
}, }],
'get-current-site': { 'get-current-site': [{
function: (message, context) => this.getCurrentSite() function: (message, context) => this.getCurrentSite()
} }]
}; };
private gcTimeout: any; private gcTimeout: any;
@ -71,19 +64,31 @@ export default class UWServer {
async setup() { async setup() {
try { try {
// logger is the first thing that goes up // logger is the first thing that goes up
const loggingOptions = BLANK_LOGGER_CONFIG; 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);
this.logAggregator = new LogAggregator('🔶bg-script🔶'); this.settings = new Settings({logger: this.logger});
this.logger = new ComponentLogger(this.logAggregator, 'UwServer', {styles: BASE_LOGGING_STYLES});
await this.logAggregator.init(loggingOptions);
this.settings = new Settings({logAggregator: this.logAggregator});
await this.settings.init(); await this.settings.init();
this.eventBus = new EventBus({isUWServer: true}); this.eventBus = new EventBus({isUWServer: true});
this.eventBus.subscribeMulti(this.eventBusCommands, this); for (const action in this.eventBusCommands) {
for (const command of this.eventBusCommands[action]) {
this.eventBus.subscribe(action, command);
}
}
this.comms = new CommsServer(this); this.comms = new CommsServer(this);
this.eventBus.setComms(this.comms); this.eventBus.setComms(this.comms);
@ -93,16 +98,12 @@ export default class UWServer {
} }
} }
async _promisifyTabsGet(browserObj, tabId){ async _promisifyTabsGet(browserObj, tabId){
return new Promise( (resolve, reject) => { return new Promise( (resolve, reject) => {
browserObj.tabs.get(tabId, (tab) => resolve(tab)); browserObj.tabs.get(tabId, (tab) => resolve(tab));
}); });
} }
//#region CSS managemeent
async injectCss(css, sender) { async injectCss(css, sender) {
if (!css) { if (!css) {
return; return;
@ -132,7 +133,7 @@ export default class UWServer {
}); });
} }
} catch (e) { } catch (e) {
this.logger.error('injectCss', 'Error while injecting css:', {error: e, css, sender}); this.logger.log('error','debug', '[UwServer::injectCss] Error while injecting css:', {error: e, css, sender});
} }
} }
async removeCss(css, sender) { async removeCss(css, sender) {
@ -162,16 +163,16 @@ export default class UWServer {
}); });
} }
} catch (e) { } catch (e) {
this.logger.error('injectCss', 'Error while removing css:', {error: e, css, sender}); this.logger.log('error','debug', '[UwServer::injectCss] Error while removing css:', {error: e, css, sender});
} }
} }
async replaceCss(oldCss, newCss, sender) { async replaceCss(oldCss, newCss, sender) {
if (oldCss !== newCss) { if (oldCss !== newCss) {
this.removeCss(oldCss, sender); this.removeCss(oldCss, sender);
this.injectCss(newCss, sender); this.injectCss(newCss, sender);
} }
} }
//#endregion
extractHostname(url){ extractHostname(url){
var hostname; var hostname;
@ -208,21 +209,20 @@ export default class UWServer {
} }
this.currentSite = this.extractHostname(tab.url); this.currentSite = this.extractHostname(tab.url);
this.logger.info('onTabSwitched', 'user switched tab. New site:', this.currentSite); this.logger.log('info', 'debug', '[UwServer::onTabSwitched] user switched tab. New site:', this.currentSite);
} catch(e) { } catch(e) {
this.logger.info('onTabSwitched', 'there was a problem getting current site:', e) this.logger.log('error', 'debug', '[UwServer::onTabSwitched] there was a problem getting currnet site:', e)
} }
this.selectedSubitem = { this.selectedSubitem = {
'siteSettings': undefined, 'siteSettings': undefined,
'videoSettings': undefined, 'videoSettings': undefined,
} }
//TODO: change extension icon based on whether there's any videos on current page //TODO: change extension icon based on whether there's any videos on current page
} }
registerVideo(sender) { registerVideo(sender) {
this.logger.info('registerVideo', 'Registering video.\nsender:', sender); this.logger.log('info', 'comms', '[UWServer::registerVideo] Registering video.\nsender:', sender);
const tabHostname = this.extractHostname(sender.tab.url); const tabHostname = this.extractHostname(sender.tab.url);
const frameHostname = this.extractHostname(sender.url); const frameHostname = this.extractHostname(sender.url);
@ -256,11 +256,11 @@ export default class UWServer {
} }
} }
this.logger.info('registerVideo', 'Video registered. current videoTabs:', this.videoTabs); this.logger.log('info', 'comms', '[UWServer::registerVideo] Video registered. current videoTabs:', this.videoTabs);
} }
unregisterVideo(sender) { unregisterVideo(sender) {
this.logger.info('unregisterVideo', 'Unregistering video.\nsender:', sender); this.logger.log('info', 'comms', '[UwServer::unregisterVideo] Unregistering video.\nsender:', sender);
if (this.videoTabs[sender.tab.id]) { if (this.videoTabs[sender.tab.id]) {
if ( Object.keys(this.videoTabs[sender.tab.id].frames).length <= 1) { if ( Object.keys(this.videoTabs[sender.tab.id].frames).length <= 1) {
delete this.videoTabs[sender.tab.id] delete this.videoTabs[sender.tab.id]
@ -270,33 +270,20 @@ export default class UWServer {
} }
} }
} }
this.logger.info('unregisterVideo', 'Video has been unregistered. Current videoTabs:', this.videoTabs); this.logger.log('info', 'comms', '[UwServer::unregisterVideo] Video has been unregistered. Current videoTabs:', this.videoTabs);
} }
setSelectedTab(menu, subitem) { setSelectedTab(menu, subitem) {
this.logger.info('setSelectedTab', 'saving selected tab for', menu, ':', subitem); this.logger.log('info', 'comms', '[UwServer::setSelectedTab] saving selected tab for', menu, ':', subitem);
this.selectedSubitem[menu] = subitem; this.selectedSubitem[menu] = subitem;
} }
async getCurrentSite() { 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( this.eventBus.send(
'set-current-site', 'set-current-site',
{ {
site, site: await this.getVideoTab(),
tabHostname, tabHostname: await this.getCurrentTabHostname(),
}, },
{ {
comms: { comms: {
@ -321,12 +308,9 @@ export default class UWServer {
return { return {
host: 'INVALID SITE', host: 'INVALID SITE',
frames: [], frames: [],
hostnames: [],
} }
} }
const hostnames = await this.comms.listUniqueFrameHosts();
if (this.videoTabs[ctab.id]) { if (this.videoTabs[ctab.id]) {
// if video is older than PageInfo's video rescan period (+ 4000ms of grace), // if video is older than PageInfo's video rescan period (+ 4000ms of grace),
// we clean it up from videoTabs[tabId].frames array. // we clean it up from videoTabs[tabId].frames array.
@ -349,7 +333,6 @@ export default class UWServer {
return { return {
...this.videoTabs[ctab.id], ...this.videoTabs[ctab.id],
host: this.extractHostname(ctab.url), host: this.extractHostname(ctab.url),
hostnames,
selected: this.selectedSubitem selected: this.selectedSubitem
}; };
} }
@ -359,8 +342,7 @@ export default class UWServer {
return { return {
host: this.extractHostname(ctab.url), host: this.extractHostname(ctab.url),
frames: [], frames: [],
hostnames, selected: this.selectedSubitem
selected: this.selectedSubitem,
} }
} }
@ -368,15 +350,12 @@ export default class UWServer {
const activeTab = await this.activeTab; const activeTab = await this.activeTab;
if (!activeTab || activeTab.length < 1) { if (!activeTab || activeTab.length < 1) {
this.logger.log('warn', 'comms', 'There is no active tab for some reason. activeTab:', activeTab);
return null; return null;
} }
const url = activeTab[0].url; const url = activeTab[0].url;
if (!url) {
console.log('no URL for active tab:', activeTab[0].url);
}
var hostname; var hostname;
if (url.indexOf("://") > -1) { //find & remove protocol (http, ftp, etc.) and get hostname if (url.indexOf("://") > -1) { //find & remove protocol (http, ftp, etc.) and get hostname

View File

@ -8,143 +8,227 @@ import SettingsInterface from '../../common/interfaces/SettingsInterface';
import { _cp } from '../../common/js/utils'; import { _cp } from '../../common/js/utils';
import CropModePersistence from '../../common/enums/CropModePersistence.enum'; import CropModePersistence from '../../common/enums/CropModePersistence.enum';
import AspectRatioType from '../../common/enums/AspectRatioType.enum'; import AspectRatioType from '../../common/enums/AspectRatioType.enum';
import { update } from 'lodash';
const ExtensionConfPatch = Object.freeze([ const ExtensionConfPatch = [
{ {
forVersion: '6.2.4', forVersion: '5.0.5',
updateFn: (userOptions: SettingsInterface, defaultOptions) => { sites: {
for (const site in userOptions.sites) { "app.plex.tv": {
userOptions.sites[site].enableUI = { mode: 3,
fullscreen: ExtensionMode.Default, autoar: 3,
theater: ExtensionMode.Default, type: "community",
normal: ExtensionMode.Default, stretch: -1,
} videoAlignment: -1,
} keyboardShortcutsEnabled: 0,
userOptions.sites['@global'].enableUI = { DOM: {
fullscreen: userOptions.ui.inPlayer.enabled ? ExtensionMode.Enabled : ExtensionMode.Disabled, player: {
theater: ExtensionMode.Enabled, manual: false,
normal: (userOptions.ui.inPlayer.enabled && !userOptions.ui.inPlayer.enabledFullscreenOnly) ? ExtensionMode.Enabled : ExtensionMode.Disabled querySelectors: "",
} additionalCss: "",
userOptions.sites['@empty'].enableUI = { useRelativeAncestor: false,
fullscreen: ExtensionMode.Default, playerNodeCss: ""
theater: ExtensionMode.Default, }
normal: ExtensionMode.Default, },
css: "body {\n background-color: #000;\n}\n\n.application {\n background-color: #000;\n}"
} }
} }
}, { }, {
forVersion: '6.2.6', forVersion: '5.0.6',
sites: {
"metaivi.com": {
mode: 0,
autoar: 0,
type: "community",
stretch: -1,
videoAlignment: -1,
DOM: {
video: {
manual: false,
querySelectors: "",
additionalCss: "position: absolute !important;"
},
player: {
manual: false,
querySelectors: "",
additionalCss: "",
useRelativeAncestor: false,
playerNodeCss: ""
}
},
"css": ""
},
"piped.kavin.rocks": {
mode: 0,
autoar: 0,
type: 'community',
autoarFallback: 0,
stretch: 0,
videoAlignment: -1,
keyboardShortcutsEnabled: 0,
DOM: {
player: {
manual: false,
querySelectors: "",
additionalCss: "",
useRelativeAncestor: false,
playerNodeCss: ""
}
},
css: ".shaka-video-container {\n flex-direction: column !important;\n}"
},
},
updateFn: (userOptions, defaultOptions) => {
// 5.0.5 initially incorrectly had app.plex.tv marked as 'user-added'
// when 'user-added' is generally reserved for marking sites with user-
// changed configuration. Site patches submitted by community should have
// 'community' type. extConfPatch for 5.0.5 was also retroactively corrected.
userOptions.sites['app.plex.tv'].type = 'community';
userOptions.sites['piped.kavin.rocks'] = {
mode: 0,
autoar: 0,
type: 'community',
autoarFallback: 0,
stretch: 0,
videoAlignment: -1,
keyboardShortcutsEnabled: 0,
DOM: {
player: {
manual: false,
querySelectors: "",
additionalCss: "",
useRelativeAncestor: false,
playerNodeCss: ""
}
},
css: ".shaka-video-container {\n flex-direction: column !important;\n}"
};
}
}, {
forVersion: '5.0.7',
updateFn: (userOptions, defaultOptions) => {
// 5.0.5 initially incorrectly had app.plex.tv marked as 'user-added'
// when 'user-added' is generally reserved for marking sites with user-
// changed configuration. Site patches submitted by community should have
// 'community' type. extConfPatch for 5.0.5 was also retroactively corrected.
userOptions.sites['www.youtube.com'].DOM.player = {
manual: true,
querySelectors: "#movie_player, #player, #c4-player",
additionalCss: "",
useRelativeAncestor: false,
playerNodeCss: "",
}
}
}, {
forVersion: '5.0.8',
updateFn: (userOptions, defaultOptions) => {
userOptions.sites['www.netflix.com'].DOM.player = {
manual: false
}
}
}, {
forVersion: '5.99.0-1',
updateFn: (userOptions: SettingsInterface, defaultOptions) => {
// add new commands
userOptions.commands = defaultOptions.commands;
}
}, {
// NOTE - when releasing shit, ensure ALL alpha migrations are combined together in one function
forVersion: '5.99.0-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: '5.99.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: '5.99.0-4',
updateFn: (userOptions: SettingsInterface, defaultOptions) => { updateFn: (userOptions: SettingsInterface, defaultOptions) => {
console.warn('[ultrawidify] STARTING SETTINGS MIGRATION TO 6.2.6'); // deprecated much?
userOptions.actions.push({
if (!userOptions.commands) { name: 'Cycle aspect ratio',
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', label: 'Cycle',
comment: 'Cycle through zoom options', cmd: [{
action: 'set-ar',
arg: AspectRatioType.Cycle
}]
});
userOptions.commands.crop.push({
action: 'set-ar',
label: 'Cycle',
comment: 'Cycle through crop options',
arguments: { arguments: {
type: AspectRatioType.Cycle type: AspectRatioType.Cycle
}, },
@ -154,93 +238,43 @@ const ExtensionConfPatch = Object.freeze([
ctrlKey: false, ctrlKey: false,
metaKey: false, metaKey: false,
altKey: false, altKey: false,
shiftKey: true, shiftKey: false,
onKeyUp: true, onKeyUp: true,
onKeyDown: false, onKeyDown: false,
} }
}, { });
action: 'set-ar-zoom', userOptions.commands.crop.push({
label: '21:9', action: 'set-ar',
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', label: '32:9',
comment: 'Zoom for 32:9 aspect ratio', comment: 'Crop for 32:9 aspect ratio',
arguments: { arguments: {
type: AspectRatioType.Fixed, type: AspectRatioType.Fixed,
ratio: 3.56 ratio: 3.56
}, },
}]; })
}
const compareShortcuts = (a: any, b: any) => { }, {
if (!a || !b) { forVersion: '5.99.5',
return false; 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 = {
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; x: VideoAlignmentType.Center,
y: VideoAlignmentType.Center
};
} }
userOptions.sites['@empty'].defaults.alignment = {x: VideoAlignmentType.Default, y: VideoAlignmentType.Default};
const hasConflict = (shortcut: any) => { }
for (const ct in userOptions.commands) { }, {
for (const command of userOptions.commands[ct]) { forVersion: '5.99.6',
if (compareShortcuts(shortcut, command.shortcut)) { updateFn: (userOptions: SettingsInterface, defaultOptions) => {
return true; for (const site in userOptions.sites) {
} userOptions.sites[site].defaultType = userOptions.sites[site].type as any;
}
}
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
});
}
} }
userOptions.sites['@global'].defaultType = 'unknown';
userOptions.sites['@empty'].defaultType = 'modified';
} }
} }
]); ];
export default ExtensionConfPatch; export default ExtensionConfPatch;

File diff suppressed because it is too large Load Diff

View File

@ -4,7 +4,6 @@ import CommsServer from './comms/CommsServer';
export interface EventBusCommand { export interface EventBusCommand {
isGlobal?: boolean, isGlobal?: boolean,
source?: any,
function: (commandData: any, context?: any) => void | Promise<void> function: (commandData: any, context?: any) => void | Promise<void>
} }
@ -19,23 +18,18 @@ export interface EventBusContext {
frame?: any, frame?: any,
sourceFrame?: IframeData sourceFrame?: IframeData
forwardTo?: 'all' | 'active' | 'contentScript' | 'server' | 'sameOrigin' | 'popup' | 'all-frames', forwardTo?: 'all' | 'active' | 'contentScript' | 'server' | 'sameOrigin' | 'popup' | 'all-frames',
};
borderCrossings?: {
commsServer?: boolean,
iframe?: boolean,
} }
} }
export default class EventBus { export default class EventBus {
private commands: { [x: string]: EventBusCommand[]} = {}; private commands: { [x: string]: EventBusCommand[]} = {};
private downstreamBuses: EventBus[] = [];
private upstreamBus?: EventBus;
private comms?: CommsClient | CommsServer; private comms?: CommsClient | CommsServer;
private disableTunnel: boolean = false; private disableTunnel: boolean = false;
private popupContext: any = {}; private popupContext: any = {};
private iframeForwardingList: {iframe: any, fn: (action, payload, context?) => void}[] = [];
// private uiUri = window.location.href; // private uiUri = window.location.href;
constructor(options?: {isUWServer?: boolean}) { constructor(options?: {isUWServer?: boolean}) {
@ -52,6 +46,9 @@ export default class EventBus {
//#region lifecycle //#region lifecycle
destroy() { destroy() {
this.commands = null; this.commands = null;
for (const bus of this.downstreamBuses) {
bus.destroy();
}
this.destroyIframeTunnelling(); this.destroyIframeTunnelling();
} }
//#endregion //#endregion
@ -60,6 +57,38 @@ export default class EventBus {
this.comms = comms; this.comms = comms;
} }
//#region bus hierarchy management (single page)
setUpstreamBus(eventBus: EventBus, stopRecursing: boolean = false) {
this.upstreamBus = eventBus;
if (!stopRecursing) {
this.upstreamBus.addDownstreamBus(this, true);
}
}
unsetUpstreamBus(stopRecursing: boolean = false) {
if (!stopRecursing) {
this.upstreamBus.removeDownstreamBus(this, false);
}
this.upstreamBus = undefined;
}
addDownstreamBus(eventBus: EventBus, stopRecursing: boolean = false) {
if (!this.downstreamBuses.includes(eventBus)) {
this.downstreamBuses.push(eventBus);
if (!stopRecursing) {
eventBus.setUpstreamBus(this, true);
}
}
}
removeDownstreamBus(eventBus: EventBus, stopRecursing: boolean = false) {
this.downstreamBuses = this.downstreamBuses.filter(x => x !== eventBus);
if (!stopRecursing) {
eventBus.unsetUpstreamBus(true);
}
}
subscribe(commandString: string, command: EventBusCommand) { subscribe(commandString: string, command: EventBusCommand) {
if (!this.commands[commandString]) { if (!this.commands[commandString]) {
this.commands[commandString] = [command]; this.commands[commandString] = [command];
@ -68,43 +97,8 @@ export default class EventBus {
} }
} }
subscribeMulti(commands: {[commandString: string]: EventBusCommand}, source?: any) {
for (const key in commands) {
this.subscribe(
key,
{
...commands[key],
source: source ?? commands[key].source
}
);
}
}
/**
* Removes all commands from a given source
* @param source
*/
unsubscribeAll(source: any) {
for (const commandString in this.commands) {
this.commands[commandString] = this.commands[commandString].filter(x => x.source !== source);
}
}
forwardToIframe(iframe: any, fn: (action: string, payload: any, context?: EventBusContext) => void) {
this.cancelIframeForwarding(iframe);
this.iframeForwardingList.push({iframe, fn});
}
cancelIframeForwarding(iframe: any) {
const existingForwarding = this.iframeForwardingList.findIndex((x: any) => x.iframe === iframe);
if (existingForwarding !== -1) {
this.iframeForwardingList.splice(existingForwarding, 1);
}
}
send(command: string, commandData: any, context?: EventBusContext) { send(command: string, commandData: any, context?: EventBusContext) {
// execute commands we have subscriptions for // execute commands we have subscriptions for
if (this.commands?.[command]) { if (this.commands?.[command]) {
for (const eventBusCommand of this.commands[command]) { for (const eventBusCommand of this.commands[command]) {
eventBusCommand.function(commandData, context); eventBusCommand.function(commandData, context);
@ -113,32 +107,17 @@ export default class EventBus {
// preventing messages from flowing back to their original senders is // preventing messages from flowing back to their original senders is
// CommsServer's job. EventBus does not have enough data for this decision. // CommsServer's job. EventBus does not have enough data for this decision.
// We do, however, have enough data to prevent backflow of messages that if (this.comms) {
// crossed CommsServer once already.
if (this.comms && context?.origin !== CommsOrigin.Server && !context?.borderCrossings?.commsServer) {
this.comms.sendMessage({command, config: commandData, context}, context); 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) { if (context?.stopPropagation) {
return; return;
} }
// propagate commands across the bus
this.sendUpstream(command, commandData, context);
this.sendDownstream(command, commandData, context);
} }
//#endregion //#endregion
@ -166,6 +145,25 @@ export default class EventBus {
} }
} }
private sendDownstream(command: string, config: any, context?: EventBusContext, sourceEventBus?: EventBus) {
for (const eventBus of this.downstreamBuses) {
if (eventBus !== sourceEventBus) {
// prevent eventBus.send from auto-propagating the command
eventBus.send(command, config, {...context, stopPropagation: true});
eventBus.sendDownstream(command, config);
}
}
}
private sendUpstream(command: string, config: any, context?: EventBusContext) {
if (this.upstreamBus) {
// prevent eventBus.send from auto-propagating the command
this.upstreamBus.send(command, config, {...context, stopPropagation: true});
this.upstreamBus.sendUpstream(command, config, context);
this.upstreamBus.sendDownstream(command, config, context, this);
}
}
//#region iframe tunnelling //#region iframe tunnelling
private setupIframeTunnelling() { private setupIframeTunnelling() {
// forward messages coming from iframe tunnels // forward messages coming from iframe tunnels

View File

@ -1,33 +1,22 @@
import Debug from '../conf/Debug'; import Debug from '../conf/Debug';
import currentBrowser from '../conf/BrowserDetect';
import ExtensionConf from '../conf/ExtensionConf'; import ExtensionConf from '../conf/ExtensionConf';
import ExtensionMode from '../../common/enums/ExtensionMode.enum';
import ObjectCopy from './ObjectCopy'; import ObjectCopy from './ObjectCopy';
import StretchType from '../../common/enums/StretchType.enum'; import StretchType from '../../common/enums/StretchType.enum';
import VideoAlignmentType from '../../common/enums/VideoAlignmentType.enum';
import ExtensionConfPatch from '../conf/ExtConfPatches'; 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 SettingsInterface from '../../common/interfaces/SettingsInterface';
import AspectRatioType from '../../common/enums/AspectRatioType.enum'; import AspectRatioType from '../../common/enums/AspectRatioType.enum';
import { SiteSettings } from './settings/SiteSettings'; 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'){ if(process.env.CHANNEL !== 'stable'){
console.info("Loading Settings"); 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 { class Settings {
//#region flags //#region flags
@ -36,9 +25,7 @@ class Settings {
//#endregion //#endregion
//#region helper classes //#region helper classes
logger: Logger;
logAggregator: LogAggregator;
logger: ComponentLogger;
//#endregion //#endregion
//#region data //#region data
@ -49,24 +36,15 @@ class Settings {
//#region callbacks //#region callbacks
onSettingsChanged: any; onSettingsChanged: any;
afterSettingsSaved: any; afterSettingsSaved: any;
onChangedCallbacks: (() => void)[] = [];
afterSettingsChangedCallbacks: (() => void)[] = [];
public snapshotManager: SettingsSnapshotManager;
//#endregion //#endregion
constructor(options: SettingsOptions) { constructor(options) {
// Options: activeSettings, updateCallback, logger // Options: activeSettings, updateCallback, logger
this.logger = options.logAggregator && new ComponentLogger(options.logAggregator, 'Settings', {styles: SETTINGS_LOGGER_STYLES}) || undefined;; this.logger = options?.logger;
this.onSettingsChanged = options.onSettingsChanged; this.onSettingsChanged = options?.onSettingsChanged;
this.afterSettingsSaved = options.afterSettingsSaved; this.afterSettingsSaved = options?.afterSettingsSaved;
this.active = options.activeSettings ?? undefined; this.active = options?.activeSettings ?? undefined;
this.default = ExtensionConf; this.default = ExtensionConf;
this.snapshotManager = new SettingsSnapshotManager();
this.default['version'] = this.getExtensionVersion(); this.default['version'] = this.getExtensionVersion();
chrome.storage.onChanged.addListener((changes, area) => {this.storageChangeListener(changes, area)}); chrome.storage.onChanged.addListener((changes, area) => {this.storageChangeListener(changes, area)});
@ -76,38 +54,22 @@ class Settings {
if (!changes.uwSettings) { if (!changes.uwSettings) {
return; return;
} }
this.logger?.info('storageOnChange', "Settings have been changed outside of here. Updating active settings. Changes:", changes, "storage area:", area); this.logger?.log('info', 'settings', "[Settings::<storage/on change>] Settings have been changed outside of here. Updating active settings. Changes:", changes, "storage area:", area);
// if (changes['uwSettings'] && changes['uwSettings'].newValue) { // if (changes['uwSettings'] && changes['uwSettings'].newValue) {
// this.logger?.log('info', 'settings',"[Settings::<storage/on change>] new settings object:", JSON.parse(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); const parsedSettings = JSON.parse(changes.uwSettings.newValue);
this.setActive(parsedSettings); this.setActive(parsedSettings);
this.logger?.info('storageOnChange', 'Does parsedSettings.preventReload exist?', parsedSettings.preventReload, "Does callback exist?", !!this.onSettingsChanged); this.logger?.log('info', 'debug', 'Does parsedSettings.preventReload exist?', parsedSettings.preventReload, "Does callback exist?", !!this.onSettingsChanged);
if (!parsedSettings.preventReload) { if (!parsedSettings.preventReload && this.onSettingsChanged) {
try { try {
for (const fn of this.onChangedCallbacks) { this.onSettingsChanged();
try {
fn();
} catch (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?.info('storageOnChange', 'Update callback finished.') this.logger?.log('info', 'settings', '[Settings] Update callback finished.')
} catch (e) { } catch (e) {
this.logger?.error('storageOnChange', "CALLING UPDATE CALLBACK FAILED. Reason:", e) this.logger?.log('error', 'settings', "[Settings] CALLING UPDATE CALLBACK FAILED. Reason:", e)
}
}
for (const fn of this.afterSettingsChangedCallbacks) {
try {
fn();
} catch (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) { if (this.afterSettingsSaved) {
@ -190,38 +152,33 @@ class Settings {
return patchesIn.sort( (a, b) => this.compareExtensionVersions(a.forVersion, b.forVersion)); return patchesIn.sort( (a, b) => this.compareExtensionVersions(a.forVersion, b.forVersion));
} }
private findFirstNecessaryPatch(version) { private findFirstNecessaryPatch(version, extconfPatches) {
return ExtensionConfPatch.findIndex(x => this.compareExtensionVersions(x.forVersion, version) > 0); const sorted = this.sortConfPatches(extconfPatches);
return sorted.findIndex(x => this.compareExtensionVersions(x.forVersion, version) > 0);
} }
private applySettingsPatches(oldVersion) { private applySettingsPatches(oldVersion, patches) {
let index = this.findFirstNecessaryPatch(oldVersion); let index = this.findFirstNecessaryPatch(oldVersion, patches);
if (index === -1) { if (index === -1) {
this.logger?.info('applySettingsPatches','There are no pending conf patches.'); this.logger?.log('info','settings','[Settings::applySettingsPatches] There are no pending conf patches.');
return; return;
} }
// save current settings object
const currentSettings = this.active;
this.snapshotManager.createSnapshot(
JSON.parse(JSON.stringify(currentSettings)),
{
label: 'Pre-migration snapshot',
isAutomatic: true
}
);
// apply all remaining patches // apply all remaining patches
this.logger?.info('applySettingsPatches', `There are ${ExtensionConfPatch.length - index} settings patches to apply`); this.logger?.log('info', 'settings', `[Settings::applySettingsPatches] There are ${patches.length - index} settings patches to apply`);
while (index < ExtensionConfPatch.length) { while (index < patches.length) {
const updateFn = ExtensionConfPatch[index].updateFn; const updateFn = patches[index].updateFn;
delete patches[index].forVersion;
delete patches[index].updateFn;
if (Object.keys(patches[index]).length > 0) {
ObjectCopy.overwrite(this.active, patches[index]);
}
if (updateFn) { if (updateFn) {
try { try {
updateFn(this.active, this.getDefaultSettings()); updateFn(this.active, this.getDefaultSettings());
} catch (e) { } catch (e) {
this.logger?.error('applySettingsPatches', 'Failed to execute update function. Keeping settings object as-is. Error:', e); this.logger?.log('error', 'settings', '[Settings::applySettingsPatches] Failed to execute update function. Keeping settings object as-is. Error:', e);
} }
} }
@ -230,35 +187,36 @@ class Settings {
} }
async init() { async init() {
let settings = await this.get(); const 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(); this.version = this.getExtensionVersion();
// |—> on first setup, settings is undefined & settings.version is haram // |—> on first setup, settings is undefined & settings.version is haram
// | since new installs ship with updates by default, no patching is // | since new installs ship with updates by default, no patching is
// | needed. In this case, we assume we're on the current version // | needed. In this case, we assume we're on the current version
const oldVersion = settings?.version ?? this.version; const oldVersion = (settings && settings.version) || this.version;
if (settings) { if (settings) {
this.logger?.info('init', "Configuration fetched from storage:", settings, this.logger?.log('info', 'settings', "[Settings::init] Configuration fetched from storage:", settings,
"\nlast saved with:", settings.version, "\nlast saved with:", settings.version,
"\ncurrent version:", this.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 there's no settings saved, return default settings.
if(! settings || (Object.keys(settings).length === 0 && settings.constructor === Object)) { if(! settings || (Object.keys(settings).length === 0 && settings.constructor === Object)) {
this.logger?.info( this.logger?.log(
'init', 'info',
'settings don\'t exist. Using defaults.\n#keys:', 'settings',
'[Settings::init] settings don\'t exist. Using defaults.\n#keys:',
settings ? Object.keys(settings).length : 0, settings ? Object.keys(settings).length : 0,
'\nsettings:', '\nsettings:',
settings settings
@ -266,7 +224,6 @@ class Settings {
this.active = this.getDefaultSettings(); this.active = this.getDefaultSettings();
this.active.version = this.version; this.active.version = this.version;
await this.save(); await this.save();
return this.active; return this.active;
} }
@ -283,7 +240,7 @@ class Settings {
// check if extension has been updated. If not, return settings as they were retrieved // check if extension has been updated. If not, return settings as they were retrieved
if (this.active.version === this.version) { if (this.active.version === this.version) {
this.logger?.info('init', "extension was saved with current version of ultrawidify. Returning object as-is."); this.logger?.log('info', 'settings', "[Settings::init] extension was saved with current version of ultrawidify. Returning object as-is.");
return this.active; return this.active;
} }
@ -294,14 +251,14 @@ class Settings {
// if extension has been updated, update existing settings with any options added in the // 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. // new version. In addition to that, we remove old keys that are no longer used.
const patched = ObjectCopy.addNew(settings, this.default); const patched = ObjectCopy.addNew(settings, this.default);
this.logger?.info('init',"Results from ObjectCopy.addNew()?", patched, "\n\nSettings from storage", settings, "\ndefault?", this.default); this.logger?.log('info', 'settings',"[Settings.init] Results from ObjectCopy.addNew()?", patched, "\n\nSettings from storage", settings, "\ndefault?", this.default);
if (patched) { if (patched) {
this.active = patched; this.active = patched;
} }
// in case settings in previous version contained a fucky wucky, we overwrite existing settings with a patch // in case settings in previous version contained a fucky wucky, we overwrite existing settings with a patch
this.applySettingsPatches(oldVersion); this.applySettingsPatches(oldVersion, ExtensionConfPatch);
// set 'whatsNewChecked' flag to false when updating, always // set 'whatsNewChecked' flag to false when updating, always
this.active.whatsNewChecked = false; this.active.whatsNewChecked = false;
@ -312,25 +269,26 @@ class Settings {
return this.active; return this.active;
} }
async get(): Promise<SettingsInterface | undefined> { async get() {
let ret; let ret;
ret = await chrome.storage.local.get('uwSettings'); ret = await chrome.storage.local.get('uwSettings');
this.logger?.info('get', 'Got settings:', ret && ret.uwSettings && JSON.parse(ret.uwSettings)); this.logger?.log('info', 'settings', 'Got settings:', ret && ret.uwSettings && JSON.parse(ret.uwSettings));
try { try {
return JSON.parse(ret.uwSettings) as SettingsInterface; return JSON.parse(ret.uwSettings);
} catch(e) { } catch(e) {
return undefined; return undefined;
} }
} }
async set(extensionConf, options?: SetSettingsOptions) { async set(extensionConf, options?) {
if (!options || !options.forcePreserveVersion) { if (!options || !options.forcePreserveVersion) {
extensionConf.version = this.version; extensionConf.version = this.version;
} }
this.logger?.info('set', "setting new settings:", extensionConf)
this.logger?.log('info', 'settings', "[Settings::set] setting new settings:", extensionConf)
return chrome.storage.local.set( {'uwSettings': JSON.stringify(extensionConf)}); return chrome.storage.local.set( {'uwSettings': JSON.stringify(extensionConf)});
} }
@ -339,54 +297,22 @@ class Settings {
this.active = activeSettings; this.active = activeSettings;
} }
/** async setProp(prop, value) {
* Sets value of a prop at given path. this.active[prop] = value;
* @param propPath path to property we want to set. If prop path does not exist,
* this function will recursively create it. It is assumed that uninitialized properties
* are objects.
* @param value
*/
async setProp(propPath: string | string[], value: any, options?: {forceReload?: boolean}, currentPath?: any) {
if (!Array.isArray(propPath)) {
propPath = propPath.split('.');
}
if (!currentPath) {
currentPath = this.active;
}
const currentProp = propPath.shift();
if (propPath.length) {
if (!currentPath[currentProp]) {
currentPath[currentProp] = {};
}
return this.setProp(propPath, value, options, currentPath[currentProp]);
} else {
currentPath[currentProp] = value;
if (options?.forceReload) {
return this.save();
} else {
return this.saveWithoutReload();
}
}
} }
async save(options?: SetSettingsOptions) { async save(options?) {
if (Debug.debug && Debug.storage) { if (Debug.debug && Debug.storage) {
console.log("[Settings::save] Saving active settings — save options", options, "; settings:", this.active); console.log("[Settings::save] Saving active settings:", this.active);
} }
this.active.preventReload = undefined; this.active.preventReload = undefined;
this.active.lastModified = new Date();
await this.set(this.active, options); await this.set(this.active, options);
} }
async saveWithoutReload(options?: SetSettingsOptions) { async saveWithoutReload() {
this.active.preventReload = true; this.active.preventReload = true;
this.active.lastModified = new Date(); await this.set(this.active);
await this.set(this.active, options);
} }
async rollback() { async rollback() {
@ -397,6 +323,22 @@ class Settings {
return JSON.parse(JSON.stringify(this.default)); return JSON.parse(JSON.stringify(this.default));
} }
getDefaultOption(option?) {
const allDefault = {
mode: ExtensionMode.Default,
autoar: ExtensionMode.Default,
autoarFallback: ExtensionMode.Default,
stretch: StretchType.Default,
videoAlignment: VideoAlignmentType.Default,
};
if (!option || allDefault[option] === undefined) {
return allDefault;
}
return allDefault[option];
}
/** /**
* Gets default site configuration. Only returns essential settings. * Gets default site configuration. Only returns essential settings.
* @returns * @returns
@ -416,19 +358,6 @@ class Settings {
getSiteSettings(site: string = window.location.hostname): SiteSettings { getSiteSettings(site: string = window.location.hostname): SiteSettings {
return new SiteSettings(this, site); return new SiteSettings(this, site);
} }
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; export default Settings;

File diff suppressed because it is too large Load Diff

View File

@ -1,418 +0,0 @@
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

@ -1,112 +0,0 @@
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

@ -1,7 +0,0 @@
enum AardMode {
Continuous = 0,
UntilDetection = 1,
Timeout
}
export default AardMode;

View File

@ -1,19 +0,0 @@
import { AardPerformanceMeasurement } from './aard-performance-measurements.enum';
export interface AardPerformanceData {
total: AardPerformanceMeasurement,
theoretical: AardPerformanceMeasurement,
imageDraw: AardPerformanceMeasurement
blackFrameDraw: AardPerformanceMeasurement,
blackFrame: AardPerformanceMeasurement,
fastLetterbox: AardPerformanceMeasurement,
edgeDetect: AardPerformanceMeasurement,
imageDrawCount: number,
blackFrameDrawCount: number,
blackFrameCount: number,
fastLetterboxCount: number,
edgeDetectCount: number,
aardActive: boolean, // whether autodetection is currently running or not
}

View File

@ -1,6 +0,0 @@
export interface AardPerformanceMeasurement {
sampleCount: number,
averageTime: number,
worstTime: number,
stDev: number,
}

View File

@ -1,6 +0,0 @@
export enum Corner {
TopLeft = 0,
TopRight = 1,
BottomLeft = 2,
BottomRight = 3,
}

View File

@ -1,7 +0,0 @@
export enum VideoPlaybackState {
NotInitialized,
Playing,
Paused,
Ended,
Error
}

View File

@ -1,37 +0,0 @@
import { GlCanvas, GlCanvasOptions } from './GlCanvas';
export class FallbackCanvas extends GlCanvas {
get type() {
return 'legacy';
}
context: CanvasRenderingContext2D;
constructor(options: GlCanvasOptions) {
super(options);
}
/**
* We need to override the following methods in order to avoid default behaviour,
* since these methods in GlCanvas touch some webGL properties that we cannot really
* touch in this class.
*/
destroy() { }
protected initContext() {
this.context = this.canvas.getContext('2d', {desynchronized: true});
}
protected initWebgl() { }
drawVideoFrame(video: HTMLVideoElement) {
this.context.drawImage(video, 0, 0, this.context.canvas.width, this.context.canvas.height);
}
getImageData() {
this.frameBuffer = this.context.getImageData(0,0,this.context.canvas.width,this.context.canvas.height).data as any as Uint8Array;
return this.frameBuffer;
}
}

View File

@ -1,393 +0,0 @@
import { mat4 } from 'gl-matrix';
import { GlCanvasBuffers, initBuffers } from './gl-init';
export interface GlCanvasOptions {
width: number;
height: number;
id?: string;
}
// Vertex shader program
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 = aTextureCoord;
}
`;
// Fragment shader program
const fsSource = `
varying highp vec2 vTextureCoord;
uniform sampler2D uSampler;
void main(void) {
highp vec4 texelColor = texture2D(uSampler, vTextureCoord);
gl_FragColor = vec4(texelColor.rgb, texelColor.a);
}
`;
interface GlCanvasProgramInfo {
program: WebGLProgram;
attribLocations: {
vertexPosition: number;
vertexNormal: number;
textureCoord: number;
};
uniformLocations: {
projectionMatrix: WebGLUniformLocation;
modelViewMatrix: WebGLUniformLocation;
normalMatrix: WebGLUniformLocation;
uSampler: WebGLUniformLocation;
};
}
export class GlCanvas {
get type() {
return 'webgl';
}
private _canvas: HTMLCanvasElement;
private set canvas(x: HTMLCanvasElement) {
this._canvas = x;
};
public get canvas(): HTMLCanvasElement {
return this._canvas;
};
private _context: WebGLRenderingContext;
private set gl(x: WebGLRenderingContext) {
this._context = x;
};
protected get gl(): WebGLRenderingContext {
return this._context;
}
private frameBufferSize: number;
private _frameBuffer: Uint8Array;
protected set frameBuffer(x: Uint8Array) {
this._frameBuffer = x;
}
public get frameBuffer(): Uint8Array {
return this._frameBuffer;
}
private buffers: GlCanvasBuffers;
private texture: WebGLTexture;
protected programInfo: GlCanvasProgramInfo;
private projectionMatrix: mat4;
get width() {
return this.canvas.width;
}
get height() {
return this.canvas.height;
}
constructor(options: GlCanvasOptions) {
this.canvas = document.createElement('canvas');
this.canvas.setAttribute('width', `${options.width}`);
this.canvas.setAttribute('height', `${options.height}`);
this.initContext(options);
this.initWebgl();
}
/**
* Draws video frame to the GL canvas
* @param video video to extract a frame from
*/
drawVideoFrame(video: HTMLVideoElement | HTMLCanvasElement): void {
this.updateTexture(video);
this.drawScene();
}
/**
* Reads pixels from the canvas into framebuffer, and returns pointer to the framebuffer
* @returns
*/
getImageData(): Uint8Array {
this.gl.readPixels(0, 0, this.canvas.width, this.canvas.height, this.gl.RGBA, this.gl.UNSIGNED_BYTE, this.frameBuffer);
return this.frameBuffer;
}
showCanvas() {
this.canvas.style.display = 'block';
this.canvas.style.position = 'fixed';
this.canvas.style.left = '0px';
this.canvas.style.top = '0px';
this.canvas.style.border = '1px dotted red';
this.canvas.style.zIndex = '1000000';
document.body.appendChild(this.canvas);
}
hideCanvas() {
this.canvas.style.display = '';
this.canvas.style.position = '';
this.canvas.style.left = '';
this.canvas.style.top = '';
this.canvas.remove();
}
/**
* Cleans up after itself
*/
destroy() {
this.gl.deleteProgram(this.programInfo.program);
this.gl.deleteBuffer(this.buffers.position);
this.gl.deleteBuffer(this.buffers.normal);
this.gl.deleteBuffer(this.buffers.textureCoord);
this.gl.deleteBuffer(this.buffers.indices);
this.gl.deleteTexture(this.texture);
}
protected initContext(options: GlCanvasOptions) {
this.gl = this.canvas.getContext(
'webgl2',
{
preserveDrawingBuffer: true
}
);
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);
}
this.frameBufferSize = options.width * options.height * 4;
}
protected initWebgl() {
// Initialize the GL context
this.gl.clearColor(0.0, 0.0, 0.0, 1.0);
this.gl.clear(this.gl.COLOR_BUFFER_BIT);
// Create shader program
const shaderProgram = this.initShaderProgram();
// Setup params for shader program
this.programInfo = {
program: shaderProgram,
attribLocations: {
vertexPosition: this.gl.getAttribLocation(shaderProgram, "aVertexPosition"),
vertexNormal: this.gl.getAttribLocation(shaderProgram, "aVertexNormal"),
textureCoord: this.gl.getAttribLocation(shaderProgram, "aTextureCoord"),
},
uniformLocations: {
projectionMatrix: this.gl.getUniformLocation(
shaderProgram,
"uProjectionMatrix"
),
modelViewMatrix: this.gl.getUniformLocation(shaderProgram, "uModelViewMatrix"),
normalMatrix: this.gl.getUniformLocation(shaderProgram, "uNormalMatrix"),
uSampler: this.gl.getUniformLocation(shaderProgram, "uSampler"),
},
};
// Here's where we call the routine that builds all the
// objects we'll be drawing.
this.buffers = initBuffers(this.gl);
this.initTexture();
// Flip image pixels into the bottom-to-top order that WebGL expects.
this.gl.pixelStorei(this.gl.UNPACK_FLIP_Y_WEBGL, true);
// Since our matrix is never going to change, we can define projection outside of drawScene function:
this.projectionMatrix = mat4.create();
mat4.ortho(this.projectionMatrix, -1, 1, -1, 1, -10, 10);
// we will be reusing our frame buffer for all draws and reads
// this improves performance and lessens production of garbage,
// translating into fewer garbage collections (probably), resulting
// in fewer hitches and other performance issues (probably)
this.frameBuffer = new Uint8Array(this.frameBufferSize);
}
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)) {
console.warn('DEBUG: Shader Compilation Error: ', type, this.gl.getShaderInfoLog(shader), '(cheat sheet: vertex shaders:', this.gl.VERTEX_SHADER, ')');
return null;
}
return shader;
}
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);
this.gl.attachShader(shaderProgram, fragmentShader);
this.gl.linkProgram(shaderProgram);
// 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;
}
return shaderProgram;
}
private initTexture(): void {
this.texture = this.gl.createTexture();
this.gl.bindTexture(this.gl.TEXTURE_2D, this.texture);
// Because video has to be download over the internet
// they might take a moment until it's ready so
// put a single pixel in the texture so we can
// use it immediately.
const level = 0;
const internalFormat = this.gl.RGBA;
const width = 1;
const height = 1;
const border = 0;
const srcFormat = this.gl.RGBA;
const srcType = this.gl.UNSIGNED_BYTE;
const pixel = new Uint8Array([0, 0, 255, 255]); // opaque blue
this.gl.texImage2D(
this.gl.TEXTURE_2D,
level,
internalFormat,
width,
height,
border,
srcFormat,
srcType,
pixel
);
// Turn off mips and set wrapping to clamp to edge so it
// will work regardless of the dimensions of the video.
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE);
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE);
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.LINEAR);
}
protected updateTexture(video: HTMLVideoElement | HTMLCanvasElement | null) {
const level = 0;
const internalFormat = this.gl.RGBA;
const srcFormat = this.gl.RGBA;
const srcType = this.gl.UNSIGNED_BYTE;
this.gl.bindTexture(this.gl.TEXTURE_2D, this.texture);
this.gl.texImage2D(
this.gl.TEXTURE_2D,
level,
internalFormat,
srcFormat,
srcType,
video
);
}
private setTextureAttribute() {
const num = 2; // every coordinate composed of 2 values
const type = this.gl.FLOAT; // the data in the buffer is 32-bit float
const normalize = false; // don't normalize
const stride = 0; // how many bytes to get from one set to the next
const offset = 0; // how many bytes inside the buffer to start from
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.buffers.textureCoord);
this.gl.vertexAttribPointer(
this.programInfo.attribLocations.textureCoord,
num,
type,
normalize,
stride,
offset
);
this.gl.enableVertexAttribArray(this.programInfo.attribLocations.textureCoord);
}
private setPositionAttribute() {
const numComponents = 3;
const type = this.gl.FLOAT; // the data in the buffer is 32bit floats
const normalize = false; // don't normalize
const stride = 0; // how many bytes to get from one set of values to the next
// 0 = use type and numComponents above
const offset = 0; // how many bytes inside the buffer to start from
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.buffers.position);
this.gl.vertexAttribPointer(
this.programInfo.attribLocations.vertexPosition,
numComponents,
type,
normalize,
stride,
offset
);
this.gl.enableVertexAttribArray(this.programInfo.attribLocations.vertexPosition);
}
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
* being rendered to the canvas, we can avoid some things, such as:
* * clearing the canvas
* * any sort of depth tests
*/
// Tell WebGL how to pull out the positions from the position
// buffer into the vertexPosition attribute.
this.setPositionAttribute();
this.setTextureAttribute();
// Tell WebGL which indices to use to index the vertices, and to use
// our program when drawing video frame to the canvas
this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, this.buffers.indices);
this.gl.useProgram(this.programInfo.program);
// Set the shader uniforms
this.gl.uniformMatrix4fv(
this.programInfo.uniformLocations.projectionMatrix,
false,
this.projectionMatrix
);
// Tell WebGL we want to affect texture unit 0, bind texture to texture unit 0,
// and tell the shader that we bound the texture to texture unit 0.
this.gl.activeTexture(this.gl.TEXTURE0);
this.gl.bindTexture(this.gl.TEXTURE_2D, this.texture);
this.gl.uniform1i(this.programInfo.uniformLocations.uSampler, 0);
// draw geometry
const vertexCount = 6;
const type = this.gl.UNSIGNED_SHORT;
const offset = 0;
this.gl.drawElements(this.gl.TRIANGLES, vertexCount, type, offset);
}
}

View File

@ -1,162 +0,0 @@
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

@ -1,83 +0,0 @@
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,84 +0,0 @@
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.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0, 1.0, 1.0, -1.0, 1.0, 1.0,
];
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 textureCoordinates = [
0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0,
];
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array(textureCoordinates),
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,7 +0,0 @@
import { GlCanvas } from '../gl/GlCanvas';
import { GlDebugCanvas } from '../gl/GlDebugCanvas';
export interface AardCanvasStore {
main: GlCanvas;
debug?: GlDebugCanvas;
}

View File

@ -1,77 +0,0 @@
/**
* Used to store coordinates of sample columns/rows and the
* first x/y position where non-black pixels were detected.
*
* Arrays are laid out like so:
*
* We check each row at these positions (columns)
* V V V
* _________ _________ _________ _____
* | x | y | x | y | x | y | ..
* '''''''''' ''''''''' ''''''''' '''''
* A A A
* If checked pixel is non-black, we put current row into
* this element of the array.
*
*/
export interface AardDetectionSample {
top?: Int16Array;
bottom?: Int16Array;
left?: Int16Array;
right?: Int16Array;
}
export function generateSampleArray(samples: number, width: number, topBottom: boolean = true) {
const sampleStore = new Int16Array(samples * 2);
/**
* We want to reverse-fencepost here.
*
* Normally, our sample positions would look like this:
*
*
* 0 1 2 3
* | :
* ||||:
* | < 20 units > :
* 0 19
*
* But we'd rather our samples are center-justified.
* We can solve this issue by dividing the width into
* (samples + 1) slices, and ignoring the first (0)
* position:
*
* 0 1 2 3
* :
* :||||:
* : :
* 0 19
*
*/
const sampleInterval = ~~(width / ( samples + 1 ));
let i = 0, col = 1;
while (i < sampleStore.length) {
sampleStore[i] = sampleInterval * col * (+topBottom * 4);
i++;
// initialize to -1 (invalid result)
sampleStore[i] = -1;
i++;
col++;
}
return sampleStore;
}
export function resetSamples(samples: AardDetectionSample) {
samples.top && resetArray(samples.top);
samples.bottom && resetArray(samples.bottom);
samples.left && resetArray(samples.left);
samples.right && resetArray(samples.right);
}
function resetArray(x: Int16Array) {
for (let i = 1; i < x.length; i+= 2) {
x[i] = -1;
}
}

View File

@ -1,54 +0,0 @@
export interface AardGradientSamples {
top: Array<Uint8Array>,
bottom: Array<Uint8Array>,
left?: Array<Uint8Array>,
right?: Array<Uint8Array>,
}
export interface AardGradientSampleOptions {
aspectRatioSamples: number;
gradientSamples: number,
}
function generateArray(samplingOptions: AardGradientSampleOptions) {
const arr = new Array<Uint8Array>(samplingOptions.aspectRatioSamples)
for (let i = 0; i < samplingOptions.aspectRatioSamples; i++) {
arr[i] = new Uint8Array(samplingOptions.gradientSamples);
}
return arr;
}
export function initAardGradientSamples(letterboxSamplingOptions: AardGradientSampleOptions): AardGradientSamples {
return {
top: generateArray(letterboxSamplingOptions),
bottom: generateArray(letterboxSamplingOptions),
};
}
export function resetGradientSamples(samples: AardGradientSamples) {
for (let i = 0; i < samples.top.length; i++) {
for (let j = 0; j < samples.top[i].length; j++) {
samples.top[i][j] = 0;
}
}
for (let i = 0; i < samples.bottom.length; i++) {
for (let j = 0; j < samples.bottom[i].length; j++) {
samples.top[i][j] = 0;
}
}
if (samples.left) {
for (let i = 0; i < samples.left.length; i++) {
for (let j = 0; j < samples.left[i].length; j++) {
samples.left[i][j] = 0;
}
}
}
if (samples.right) {
for (let i = 0; i < samples.right.length; i++) {
for (let j = 0; j < samples.right[i].length; j++) {
samples.right[i][j] = 0;
}
}
}
}

View File

@ -1,18 +0,0 @@
import {VideoPlaybackState} from '../enums/video-playback-state.enum';
export interface AardStatus {
aardActive: boolean,
aardReducedPolling: boolean,
checkInProgress: boolean,
lastVideoStatus: VideoPlaybackState,
}
export function initAardStatus(): AardStatus {
return {
aardActive: false,
aardReducedPolling: true,
checkInProgress: false,
lastVideoStatus: VideoPlaybackState.NotInitialized,
}
}

View File

@ -1,127 +0,0 @@
import { AardSettings } from '../../../../common/interfaces/SettingsInterface'
export interface AardTestResults {
isFinished: boolean,
lastStage: number,
notLetterbox: boolean,
blackLevel: number, // is cumulative
blackThreshold: number, // is cumulative
guardLine: {
top: number, // is cumulative
bottom: number, // is cumulative
invalidated: boolean,
cornerViolated: [boolean, boolean, boolean, boolean],
cornerPixelsViolated: [0,0,0,0]
},
imageLine: {
top: number, // is cumulative
bottom: number, // is cumulative
invalidated: boolean
},
aspectRatioCheck: {
topRows: [number, number, number],
topQuality: [number, number, number],
bottomRows: [number, number, number],
bottomQuality: [number, number, number],
topCandidate: number,
topCandidateQuality: number,
bottomCandidate: number,
bottomCandidateDistance: number,
bottomCandidateQuality: number,
topRowsDifferenceMatrix: [number, number, number],
bottomRowsDifferenceMatrix: [number, number, number],
},
aspectRatioUncertain: boolean,
topRowUncertain: boolean,
bottomRowUncertain: boolean,
aspectRatioUpdated: boolean,
activeAspectRatio: number, // is cumulative
letterboxWidth: number,
letterboxOffset: number,
logoDetected: [boolean, boolean, boolean, boolean]
aspectRatioInvalid: boolean
aspectRatioUncertainReason?: string
aspectRatioInvalidReason?: string
}
export function initAardTestResults(settings: AardSettings): AardTestResults {
return {
isFinished: true,
lastStage: 0,
notLetterbox: false,
blackLevel: settings.blackLevels.defaultBlack,
blackThreshold: 16,
guardLine: {
top: -1,
bottom: -1,
invalidated: false,
cornerViolated: [false, false, false, false],
cornerPixelsViolated: [0,0,0,0]
},
imageLine: {
top: -1,
bottom: -1,
invalidated: false,
},
aspectRatioCheck: {
topRows: [-1, -1, -1],
topQuality: [0, 0, 0],
bottomRows: [-1, -1, -1],
bottomQuality: [0, 0, 0],
topCandidate: 0,
topCandidateQuality: 0,
bottomCandidate: 0,
bottomCandidateDistance: 0,
bottomCandidateQuality: 0,
topRowsDifferenceMatrix: [0, 0, 0],
bottomRowsDifferenceMatrix: [0, 0, 0],
},
aspectRatioUncertain: false,
topRowUncertain: false,
bottomRowUncertain: false,
aspectRatioUpdated: false,
activeAspectRatio: 0,
letterboxWidth: 0,
letterboxOffset: 0,
logoDetected: [false, false, false, false],
aspectRatioInvalid: false,
}
}
export function resetGuardLine(results: AardTestResults) {
results.guardLine.top = -1;
results.guardLine.bottom = -1;
results.imageLine.invalidated = false;
results.guardLine.invalidated = false;
results.guardLine.cornerViolated[0] = false;
results.guardLine.cornerViolated[1] = false;
results.guardLine.cornerViolated[2] = false;
results.guardLine.cornerViolated[3] = false;
results.guardLine.cornerPixelsViolated[0] = 0;
results.guardLine.cornerPixelsViolated[1] = 0;
results.guardLine.cornerPixelsViolated[2] = 0;
results.guardLine.cornerPixelsViolated[3] = 0;
}
export function resetAardTestResults(results: AardTestResults): void {
results.isFinished = false;
results.lastStage = 0;
results.notLetterbox = false;
results.imageLine.invalidated = false;
results.guardLine.invalidated = false;
results.guardLine.cornerViolated[0] = false;
results.guardLine.cornerViolated[1] = false;
results.guardLine.cornerViolated[2] = false;
results.guardLine.cornerViolated[3] = false;
results.guardLine.cornerPixelsViolated[0] = 0;
results.guardLine.cornerPixelsViolated[1] = 0;
results.guardLine.cornerPixelsViolated[2] = 0;
results.guardLine.cornerPixelsViolated[3] = 0;
// results.letterboxWidth = 0;
// results.letterboxOffset = 0;
results.aspectRatioUpdated = false;
results.aspectRatioUncertainReason = null;
results.topRowUncertain = false;
results.bottomRowUncertain = false;
results.aspectRatioInvalid = false;
}

View File

@ -1,11 +0,0 @@
export interface AardTimers {
nextFrameCheckTime: number;
reducedPollingNextCheckTime: number;
}
export function initAardTimers(): AardTimers {
return {
nextFrameCheckTime: 0,
reducedPollingNextCheckTime: 0,
};
}

View File

@ -18,19 +18,15 @@ export enum AngleVersion {
* @returns * @returns
*/ */
function detectDX(shaderSource: string) { function detectDX(shaderSource: string) {
try { const glsl = shaderSource.match(/#version (\d+)( es)?$/m);
const glsl = shaderSource.match(/#version (\d+)( es)?$/m);
const glslVer = +glsl[1]; const glslVer = +glsl[1];
if (glslVer >= 300) { if (glslVer >= 300) {
return AngleVersion.D3D11; return AngleVersion.D3D11;
} }
if (glslVer >= 100) { if (glslVer >= 100) {
return AngleVersion.D3D9; return AngleVersion.D3D9;
}
} catch (e) {
return AngleVersion.NotAvailable;
} }
} }
@ -72,19 +68,16 @@ function detectGl(shaderSource: string) {
* @returns * @returns
*/ */
function detectBackend(str) { function detectBackend(str) {
try {
if (str.match(/metal::float4/)) {
return AngleVersion.Metal;
}
if (str.match(/VS_OUTPUT main\(/)) { if (str.match(/metal::float4/)) {
return detectDX(str); return AngleVersion.Metal;
}
return detectGl(str);
} catch (e) {
return AngleVersion.NotAvailable;
} }
if (str.match(/VS_OUTPUT main\(/)) {
return detectDX(str);
}
return detectGl(str);
} }
/** /**

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More