Compare commits

..

No commits in common. "master" and "4.5.1" have entirely different histories.

303 changed files with 30461 additions and 48804 deletions

View File

@ -6,9 +6,9 @@
["@babel/preset-env", {
"useBuiltIns": false,
"targets": {
"esmodules": true
}
}]
"esmodules": true,
},
}],
]
}
// {

2
.gitignore vendored
View File

@ -13,5 +13,3 @@ build/
src/res/img/git-ignore/
test/debug-configs/
debugging-resources/

23
.gitlab-ci.yml Normal file
View File

@ -0,0 +1,23 @@
image: node:current
cache:
paths:
- node_modules/
- .yarn
stages:
- prep
- build
- pack
install deps:
stage: prep
script: yarn install
build:
stage: build
script: npm run build
create zip:
stage: pack
script: npm run build-zip

2
.nvmrc
View File

@ -1 +1 @@
20.19.6
14.4.0

View File

@ -1,8 +0,0 @@
{
"recommendations": [
"hollowtree.vue-pack",
"msjsdiag.debugger-for-chrome",
"firefox-devtools.vscode-firefox-debug",
"msjsdiag.debugger-for-edge"
]
}

18
.vscode/launch.json vendored
View File

@ -4,19 +4,10 @@
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"request": "attach",
"name": "Attach to Chrome",
"port": 9222,
"urlFilter": "http://*/*",
"webRoot": "${workspaceFolder}"
},
{
"type": "firefox",
"request": "attach",
"name": "Attach (firefox)",
"name": "Attach",
"pathMappings": [
{
"url": "webpack:///ext",
@ -25,7 +16,7 @@
]
},
{
"name": "Launch addon (firefox)",
"name": "Launch addon",
"type": "firefox",
"request": "launch",
"port": 6000,
@ -45,8 +36,5 @@
"firefoxArgs": [
"--start-debugger-server"
]
},
"chrome": {
"executable": "/usr/bin/google-chrome-stable --remote-debugging-port=9222",
}
}
}

46
.vscode/settings.json vendored
View File

@ -1,7 +1,8 @@
{
"cSpell.words": [
"PILLARBOX",
"PILLARBOXED",
"aard",
"allowtransparency",
"ardetector",
"autodetect",
"autodetection",
@ -9,77 +10,43 @@
"blackbar",
"blackbars",
"blackframe",
"bruteforce",
"canvas",
"clickthrough",
"cmpa",
"cmpb",
"com",
"comms",
"csui",
"ctab",
"cycleable",
"decycle",
"dinked",
"dinks",
"disneyplus",
"endregion",
"equalish",
"fith",
"fitw",
"fuckup",
"fucky",
"geoblocked",
"geoblocking",
"gfycat",
"gmail",
"guardline",
"han",
"haram",
"Heebo",
"hoverable",
"iframe",
"imgur",
"insta",
"jsoneditor",
"jwplayer",
"kavin",
"letterboxed",
"manjaro",
"mdicon",
"mdijs",
"metaivi",
"minification",
"mitigations",
"mstefan",
"nogrow",
"noshrink",
"outro",
"PILLARBOX",
"PILLARBOXED",
"polyfill",
"problemo",
"recursing",
"reddit",
"rescan",
"resizer",
"resizers",
"SCPI",
"scrollbar",
"shitiness",
"smallcaps",
"spaghettified",
"suboption",
"subscan",
"tabitem",
"tablist",
"tamius",
"textbox",
"ultrawide",
"ultrawidify",
"unmark",
"unmarking",
"unsets",
"unshift",
"uwid",
"uwui",
@ -87,14 +54,9 @@
"vids",
"vuejs",
"vuex",
"wakanim",
"webextension",
"webextensions",
"wuckies",
"wucky",
"ycenter",
"youtube",
"YYMM"
"youtube"
],
"cSpell.ignoreWords": [
"abcdefghijklmnopqrstuvwxyz",
@ -110,4 +72,4 @@
"undisable",
"vdid"
]
}
}

View File

@ -1,201 +1,29 @@
# Changelog
## v6.0 (current major)
### v6.4.0
* In-player UI now appears in full-screen even for websites that use top layer
* Embedded sites now inherit settings of the parent frame by default
* Setting inheritance/overriding is not thoroughly tested and may be full of edge cases.
* Added validation to custom aspect ratio entry menu. Corrected parsing of aspect ratios given in the X:Y format, even though aspect ratios should be ideally given as a single number.
* Autodetection can now scan for subtitles.
* New experimental autodetection (which is secretly just slightly modified subtitle check)
### v6.3.0
* Added zoom segment to in-player UI and popup.
* Fixed keyboard zoom
* Added additional zoom options. If you wonder how zoom options differ from crop, mess around and find out.
* Subdomains now inherit same settings as their parent domain by default
* Extension popup detects embedded content
* Added `www.youtube-nocookie.com` to "officially supported" list
### 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.
* In player UI
* New alignment options (can align video both vertically, as well as horizontally)
* Extension does a little bit of a better job differentiating between levels of support for a given site
* Changed how cropping and panning works. This should lead to fewer problems and better generic support.
* REGRESSION: no manual panning with shift + mouse movement
* "Player select" screen, which provides a GUI way for picking HTML element that acts as the video player area
Regressions and potential issues:
* Settings were largely not migrated from previous versions. This is as intended, since changes to cropping (and some other aspects)
rendered certain old settings obsolete.
* Extension can no longer discriminate between iframes — extension popup commands get sent to ALL iframes on page
* Extension probably can't discriminate between multiple videos on a single page
## v5.x
### v5.1.7
Firefox-only.
* When cropping and panning video, CSS' `transform: translate(x,y)` now uses integers _always_. Before that fix, `translate(x,y)` could be offset by x.5 px, and that half of a pixel introduced a 1px thick line on the portion of the left edge of the video.
### v5.1.7
* When aligning videos, ensure video is never translated by a fractional value
* As of recent nVidia driver update in Edge, Angle detection fails in a way that prevents popup from showing up. This problem should be fixed now.
The angle detection problem was fixed by disabling the angle detection check for now, as Chrome, et al. appear to have fixed buggy video hardware acceleration.
### v5.1.6
* Added new config for disney+ (courtesy of @MStefan99)
* Chrome hardware acceleration is bugged and cannot be worked around in-extension. Only way to fix this is for users to change their Google Chrome settings. Added naggathon with instructions into the extension popup.
### v5.1.5
* Fixed laginess in Chromium-based browsers on Windows. Details in [#199](https://github.com/tamius-han/ultrawidify/issues/199#issuecomment-1221383134)
### v5.1.4
* Fixed some problems with autodetection not returning to 16:9 when necessary if autodetection already changed aspect ratio ([#198](https://github.com/tamius-han/ultrawidify/issues/198))
### v5.1.3
* Fixed some problems with autodetection sometimes briefly resetting on dark frames ([#195](https://github.com/tamius-han/ultrawidify/issues/195), [#196](https://github.com/tamius-han/ultrawidify/issues/196))
### v5.1.2
* `set-extension-mode` turned into `set-ExtensionMode` at some point. This caused in "Enable this extension" options to vanish on certain setups.
* Blackframe tests now run on same data as main algorithm as opposed on a smaller sample (`drawImage()` calls are _very_ expensive even for a 16x9 sample).
### v5.1.1
* Fixed autodetection
### v5.1.0
* Re-enable logger
* Move aspect ratio autodetection to requestAnimationFrame
* Fix netflix
### v5.0.7
* Videos of square-ish aspect ratios on 1440p (and lower) resolutions now no longer get misaligned ([#162](https://github.com/tamius-han/ultrawidify/issues/162))
* Alignment of featured videos on youtube channel page should now also be fixed
### v5.0.6
* Added configuration for metaivi.com based on user feedback ([#160](https://github.com/tamius-han/ultrawidify/issues/160))
* Removed ExtConfPatches for versions < 4.5.0, because nobody should be using a build of this extension that's over a year old
### v5.0.5
* improved UX a bit
* Fixed white background on app.plex.tv ([#158](https://github.com/tamius-han/ultrawidify/issues/158))
### v5.0.4
* Attempt to fix disney+ again, courtesy of [@jwannebo](https://github.com/tamius-han/ultrawidify/issues/84#issuecomment-846334005) on github.
### v5.0.3
### Plans for the future
* Fixed the issue where the videos were sometimes offset up and left. Again.
* Fix the issue where correcting source stretch was squished incorrectly ([#153](https://github.com/tamius-han/ultrawidify/issues/153))
* Native builds for Chromium Edge
* Settings page looks ugly af right now. Maybe fix it some time later
* other bug fixes
### v5.0.2
## v5.0 (planned major)
* When in full screen, the extension will assume player element dimensions are the same as the screen resolution. This should help with sites where ultrawidify doesn't correctly identify the player, as cropping generally doesn't work if player element is not identified. Old behaviour can be restored in advanced extension settings by toggling the "use player aspect ratio in fullscreen" checkbox under 'player detection settings'.
* Extension should now respect 'disable extension' option for real.
* Fixed the issue where player wouldn't get detected if video was wider than the player.
* WebGL autodetection
* in-player GUI
* Fix UI logger
### v5.0.1
* Added an option for users to turn off (and/or configure) Chrome/Edge's zoom limiter.
### v5.0.0
There's been some big-ish changes under the hood:
* Migrate main scripts to typescript (vue is currently not included).
* webextension-polyfill is now used everywhere (if only because typescript throws a hissy fit with `browser` and `chrome` otherwise) ([#114](https://github.com/tamius-han/ultrawidify/issues/114))
* Fix some bugs that I didn't even know I had, but typescript kinda shone some light on them
* Manual zoom (Z/U unless sites override the two) should now work again (without automatic AR constantly overriding it). Same goes for panning. ([#135](https://github.com/tamius-han/ultrawidify/issues/135) & [#138](https://github.com/tamius-han/ultrawidify/issues/138))
* Fix issue when video would be scaled incorrectly if video element uses `height:auto`.
* **[5.0.0.1]** Fixed the issue where settings were reset on page load.
* **[5.0.0.1]** Fixed the issue where settings page wouldn't load.
## v4.x (current major)
### v4.5.3
* Provides workaround for the fullscreen stretching bug Chrome 88 (or a recent Windows 10 update) introduced for nVidia users using hardware acceleration on Windows 10. In order to mitigate this bug, Ultrawidify needs to keep a 5-10 px wide black border while watching videos in full screen. This bug is also present in Edge.
* **[4.5.3.1]** Fixed letterbox misalignment binding in settings (#134)
* **[4.5.3.2]** Fixed false 'autodetection not supported' notifications.
### v4.5.2
* Fixed the issue where videos would sometimes get misaligned while using hybrid stretch, except for real this time. ([#125](https://github.com/tamius-han/ultrawidify/issues/125))
* Improved DRM detection (the 'autodetection cannot work on this site' popup should now no longer show up on the sites where autodetection _can_ work)
### v4.5.1
* Fixed the misalignment issue on netflix ... hopefully.
* 'Site settings' tab should now work in Chrome as well ([#126](https://github.com/tamius-han/ultrawidify/issues/126))
* Popup interface now refreshes properly ([#127](https://github.com/tamius-han/ultrawidify/issues/127))
* Videos should now be scaled correctly when the display is narrower than video's native aspect ratio ([#118](https://github.com/tamius-han/ultrawidify/issues/118))
* Edge users using CWS version of the extension get a very aggressive warning when trying to use the extension with Edge
* Fullscreen videos on streamable are aligned correctly ([#116](https://github.com/tamius-han/ultrawidify/issues/118)).
* **[4.5.1.1]** Streamable fix broke old.reddit + RES on embeds from v.redd.it and streamable.com. We're now using an alternative implementation. ([#128](https://github.com/tamius-han/ultrawidify/issues/128))
* **[4.5.1.2]** Fixed the issue where videos would sometimes get misaligned while using hybrid stretch. ([#125](https://github.com/tamius-han/ultrawidify/issues/125))
* **[4.5.1.3]** Added fix for disney plus
* **[4.5.1.3]** Microsoft Edge has fixed the bugs that prevented the extension from working properly. Popup should no longer be shown.
### v4.5.0 (Current)
@ -207,7 +35,7 @@ There's been some big-ish changes under the hood:
* The extension will now show a notification when autodetection can't run due to DRM
* Videos on facebook and reddit no longer get shifted up and to the left for me (cropping most of the video off-screen), but I haven't been deliberately trying to fix that issue. If you experience that issue, please consider contacting me (via github or email) with a link to a problematic video.
### v4.4.10
### v4.4.10
* Video alignment should now work on Twitch — [#109](https://github.com/tamius-han/ultrawidify/issues/109)
* Videos should now align properly on Hulu while cropped — [#111](https://github.com/tamius-han/ultrawidify/issues/111) & via email
@ -245,7 +73,7 @@ In addition to that, as of 4.4.9.1 the build process ensures removal of `node_mo
* Fixed netflix (and possibly disney+ )
* It's been almost a month and Chrome Web Store still hasn't finished the review of the 4.4.4.1 (and 4.4.4.2) revisions because when it comes to incompetence, it's hard to expect anything less from Google. I've did some proverbial yelling at the support in hopes that Chrome version will finally see an update (disclaimer: when I said yelling I really mean a polite request, because support staff doesn't deserve abuse because a different department is utter shite at doing their jobs).
### v4.4.5
### v4.4.5
* Extension no longer requires `allTabs` and `webNavigation` permissions
* Some CSS on the debugger popup was not scoped, causing issues with some sites.
@ -275,7 +103,7 @@ In addition to that, as of 4.4.9.1 the build process ensures removal of `node_mo
* Changes to player detection that fix issues with vk
* Extension tries to avoid setting aspect ratio pointlessly
* (Hopefully) fixed mailto: and reddit compose links.
* (Hopefully) fixed mailto: and reddit compose links.
* When reporting bugs, email/reddit template now automatically gathers browser, extension version and OS.
### v4.4.0
@ -321,23 +149,23 @@ and OS are automatically included in email/reddit template.
* Started using mutation observers to watch for anything modifying the size of our video.
* **[4.2.3.1]** fixed some bugs in popup.
### v4.2.2
### v4.2.2
* Fixed player detection on reddit (for videos from v.reddit)
### v4.2.1
* Fixed bug where custom CSS didn't get applied to pages
### v4.2.0
### v4.2.0
* Slightly improved popup design. (Design change suggested by PortaTrekos)
* Player detection: youtube and twitch now have manual player element detection, with strictly defined players.
* Player detection: youtube and twitch now have manual player element detection, with strictly defined players.
* Improved site settings control in extension popup. It's possible to enable extension for previously disabled embedded sites.
* Improved incompatibilities with reddit, where videos would be vertically misaligned when not using RES
* Fixed imcompatibilities with Iridium. Flicker when clicking play/pause or switching between big and popup player is caused by either Youtube or Iridium trying to apply their styles over mine.
* Issues with inconsistent alignment that some people reported are potentially fixed
### v4.1.2
### v4.1.2
* Fixed video alignment issues on www.reddit as well (for people who use old reddit without going to old.reddit)
* Fixed bug with 'player detection' tab
@ -349,17 +177,17 @@ and OS are automatically included in email/reddit template.
### v4.1.0
* Added ability to add custom CSS to page
* Fixed video alignment issues on old.reddit. Disabled extension on imgur by default.
* Fixed video alignment issues on old.reddit. Disabled extension on imgur by default.
* Extension now works on vimeo again
* **UX:** Renamed 'about' to 'report a problem' in order to make contact info more discoverable
### v4.0.1
* Fixed bug where sites using 'default' option in 'Extension mode' settings would be disabled, even if extension was not.
* Fixed bug where sites using 'default' option in 'Extension mode' settings would be disabled, even if extension was not.
* Fixed bug where extension sometimes wouldn't work on Netflix.
### v4.0.0
### v4.0.0
* Fixed the bug where saving settings wouldn't work
* Massive under-the-hood changes. The extension popup and settings page use VueJS
@ -389,7 +217,7 @@ and OS are automatically included in email/reddit template.
Never happened, got bumped to 4.0.0.
### v3.2.2
### v3.2.2
* Pan event listener now gets properly unbound
* Fixed 'reset zoom' button in popup
@ -398,7 +226,7 @@ Never happened, got bumped to 4.0.0.
* Fixed issue where global video alignment setting didn't get saved properly
### v3.2.0
### v3.2.0
* Zoom and panning
* Reorganized popup
@ -438,7 +266,7 @@ User-facing changes:
Added some anti-lag measures. This seems to be an issue affecting _only_ Chrome (and only then some installs), where canvas.drawImage() won't work properly for some reason.
### v2.2.4
### v2.2.4
Lots of mostly incredibly minor stuff.
@ -449,11 +277,11 @@ Lots of mostly incredibly minor stuff.
* Fixed some under-the-hood bugs nobody knew they even existed
* A lil bit of refactoring
### v2.2.3
### v2.2.3
* Fixed automatic aspect ratio detection on DRM-protected sites.
### v2.2.2
### v2.2.2
* Fixes problems with switching from normal to fullscreen player on youtube. If 2.2.1 didn't fix the font issue, this version should have.
@ -469,9 +297,9 @@ Various improvements to automatic aspect ratio detection:
* **Fixed the situation with insane memory usage due to the automatic aspect ratio detection (#25, #32) and lag that appeared in certain cases after the extension has been running for a while.** There's still fun stuff going on — see notes below.
* Improved accuracy of automatic detection. This should fix the issue of rapid switching in dark videos or videos with otherwise uneven edges (#12 - [video](https://www.youtube.com/watch?v=NaTGwlfRB_c); #24 - [video](https://www.youtube.com/watch?v=xvZqHgFz51I) (see the car at the beginning))
Improved accuracy has increased the base RAM usage, and not by a small amount (I seem to have fixed my blunders, so that could _actually_ be on Firefox). As a result, I've reduced both resolution of the sample as well as polling frequency.
Improved accuracy has increased the base RAM usage, and not by a small amount (I seem to have fixed my blunders, so that could _actually_ be on Firefox). As a result, I've reduced both resolution of the sample as well as polling frequency.
Polling of 1 check per second shouldn't use too much RAM. If you want automatic aspect ratio detection to react faster, you can up that number to 30 in the settings. 30 checks per second can be expensive: up to 400 MB if you've just started Firefox and went to youtube. Can go north of 2 gigs if you've been running Firefox for longer than that (seems to be a problem with Javascript garbage collection).
Polling of 1 check per second shouldn't use too much RAM. If you want automatic aspect ratio detection to react faster, you can up that number to 30 in the settings. 30 checks per second can be expensive: up to 400 MB if you've just started Firefox and went to youtube. Can go north of 2 gigs if you've been running Firefox for longer than that (seems to be a problem with Javascript garbage collection).
Videos that aren't playing (e.g. videos that are paused or ended) do (should) ***not*** use any meaningful amount of RAM.
@ -489,11 +317,11 @@ Videos that aren't playing (e.g. videos that are paused or ended) do (should) **
Youtube fix seems to have broken Chrome compatibility (again), so any quick fix for this point forward will land in Chrome version along with v2.2.
### v2.1.2
### v2.1.2
* Fixed some bugs with autodetection sometimes not working properly on Youtube.
Problem: there's this bit of code that keeps aspect ratio from changing when the difference between 'previous' and 'current' aspect ratio is too small. Unfortunately, the 'previous' value was _not_ updated on every aspect ratio switch for some reason. Also `ArDetect.init()` — for some reason — didn't always clean the 'previous' value even though it should.
Problem: there's this bit of code that keeps aspect ratio from changing when the difference between 'previous' and 'current' aspect ratio is too small. Unfortunately, the 'previous' value was _not_ updated on every aspect ratio switch for some reason. Also `ArDetect.init()` — for some reason — didn't always clean the 'previous' value even though it should.
### v2.1.1
@ -505,7 +333,7 @@ Problem: there's this bit of code that keeps aspect ratio from changing when the
* Popup should work more reliably now
* Twitch works ... kinda but not always
### v2.0.3
### v2.0.3
* Fixed the bug where Netflix videos weren't vertically centered in Firefox 57+ (not present in Chrome or FF 56 or earlier)
@ -566,15 +394,15 @@ The extension is being rewritten almost ground-up, around automatic aspect ratio
* Introduced Netflix support.
As Netflix relies on extension re-initializing at least the UI ***a lot***, the optimization introduced in 1.0.2 was reversed (as waiting 2 seconds for the UI to appear is just too much).
As Netflix relies on extension re-initializing at least the UI ***a lot***, the optimization introduced in 1.0.2 was reversed (as waiting 2 seconds for the UI to appear is just too much).
Furthermore, triggering UI re-initialisation on onUpdated events turned out to not be the proper way to go: immediately after the extension is initialized, onUpdated gets triggered even more often than your average Buzzfeed writer/reader. But change the episode on Netflix and suddenly, onUpdated gets barely triggered at all — which means that more often than not, the UI extension injects into the page wasn't visible. (the fuck, really)
This is why Netflix uses another function that manually checks whether the player bar is present. Ideally that check happens every tenth of a second, but Firefox may be limiting that to one per second.
### v1.0.2
### v1.0.2
The 'extension sometimes not working' bug was fixed (by having extension try to setup every time a page got updated), but the fix had some problems. Namely, the extension would re-initiate (complete with re-adding the entire UI) itself very _very_ often.
The 'extension sometimes not working' bug was fixed (by having extension try to setup every time a page got updated), but the fix had some problems. Namely, the extension would re-initiate (complete with re-adding the entire UI) itself very _very_ often.
This could be a problem, so it was fixed. Extension is notified of updates only every ~2 seconds (which absorbs most of the "page was updated" events on page load) and doesn't attempt to reload the UI if the UI was already loaded. (Unless `debugmsg` is set to true. It's generally not, but any commits to this repo could potentially still have it enabled).
@ -588,7 +416,7 @@ Fixed the bug where sometimes the extension would fail to work. (example: you op
### v1.0-rc1
* Settings page is added and mostly working.
* Settings page is added and mostly working.
### v0.9.9.6
@ -604,7 +432,7 @@ Fixed the bug where sometimes the extension would fail to work. (example: you op
### v0.9.9.1
* Keybinds `a` and `w` now work.
* Keybinds `a` and `w` now work.
* Some changes under the bonnet, mostly regarding the way keypresses are handled.
* 'Settings' page is ~15% done.

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,6 +0,0 @@
# Implementation details
## Enabling/disabling aspect ratio corrections
* Aspect ratios are changed by proxy. Extension attaches **a custom CSS class** to `video` and `player` elements.
* To prevent extension from affecting the appearance of a webpage, **it's sufficient to remove our custom CSS classes from `video` and `player` elements.**

32
Jenkinsfile vendored
View File

@ -1,32 +0,0 @@
// required jenkins plugins:
// * https://plugins.jenkins.io/git/
pipeline {
agent any
stages {
// stage('Check for changes') {
// sh "env.GIT_COMMIT != env.GIT_PREVIOUS_COMMIT"
// }
stage('Install dependencies') {
steps {
sh 'npm ci'
}
}
stage('Build') {
steps {
sh 'npm run build-all'
}
}
stage('Push to release server') {
steps {
sh "echo 'implement me pls!'"
}
}
}
}

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.

View File

@ -1,8 +1,10 @@
# Ultrawidify — aspect ratio fixer for youtube and netflix
# Ultrawidify — aspect ratio fixer for youtube and netflix
## Super TL;DR: I'm just looking for the install links, thanks
[Firefox](https://addons.mozilla.org/en/firefox/addon/ultrawidify/), [Chrome](https://chrome.google.com/webstore/detail/ultrawidify/dndehlekllfkaijdlokmmicgnlanfjbi), [Edge](https://microsoftedge.microsoft.com/addons/detail/ultrawidify/lmpgpgechmkkkehkihpiddbcbgibokbi).
[Firefox](https://addons.mozilla.org/en/firefox/addon/ultrawidify/), [Chrome](https://chrome.google.com/webstore/detail/ultrawidify/dndehlekllfkaijdlokmmicgnlanfjbi).
**Microsoft Edge is not supported at this time, as Edge features some bugs that make it impossible for extension to work.** [Read more](https://github.com/tamius-han/ultrawidify/issues/117#issuecomment-747109695).
There's also [nightly "builds"](https://stuff.lionsarch.tamius.net/ultrawidify/nightly/).
@ -49,13 +51,13 @@ If extension doesn't work for a site I'm not testing on out of the box, follow [
### Installing this extension
You can download this extension from the relevant extension stores:
You can download this extension from Firefox' and Chrome's extension stores:
* [Firefox](https://addons.mozilla.org/en/firefox/addon/ultrawidify/)
* [Chrome](https://chrome.google.com/webstore/detail/ultrawidify/dndehlekllfkaijdlokmmicgnlanfjbi)
* [Edge](https://microsoftedge.microsoft.com/addons/detail/ultrawidify/lmpgpgechmkkkehkihpiddbcbgibokbi)
* [Chrome, Opera](https://chrome.google.com/webstore/detail/ultrawidify/dndehlekllfkaijdlokmmicgnlanfjbi)
* [Chromium Edge](https://microsoftedge.microsoft.com/addons/detail/lmpgpgechmkkkehkihpiddbcbgibokbi)
Other browsers are not officially supported. If you're using a different Chromium-based browser, you can try installing the addon from the Chrome Web Store — but if things don't work, you're on your own.
Users of beta and developer branches of Opera can find Ultrawidify on Opera store as well, but given Opera's review process is unacceptably slow (awaiting moderator review since 2020-03-15) not only am I not going to maintain Opera store presence, Opera users are advised to use Chrome version of the extension in order to avoid waiting years for new features to be approved by Opera moderators.
### Nightly builds

36618
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,88 +1,62 @@
{
"name": "ultrawidify",
"version": "6.3.998",
"version": "4.5.0",
"description": "Aspect ratio fixer for youtube and other sites, with automatic aspect ratio detection. Supports ultrawide and other ratios.",
"author": "Tamius Han <tamius.han@gmail.com>",
"scripts": {
"build": "npm run pre-build; cross-env NODE_ENV=production BROWSER=firefox CHANNEL=stable webpack",
"build-all": "bash ./scripts/build-all.sh",
"build-chrome": "cross-env NODE_ENV=production BROWSER=chrome CHANNEL=stable webpack",
"build-chrome:dev": "cross-env NODE_ENV=development BROWSER=chrome webpack",
"build-edge": "cross-env NODE_ENV=production BROWSER=edge CHANNEL=stable webpack",
"build-nightly": "cross-env NODE_ENV=development BROWSER=firefox CHANNEL=nightly webpack",
"build-nightly-chrome": "cross-env NODE_ENV=development BROWSER=chrome CHANNEL=nightly webpack",
"build-testing": "cross-env NODE_ENV=development BROWSER=firefox CHANNEL=testing webpack",
"build-testing-chrome": "cross-env NODE_ENV=development BROWSER=chrome CHANNEL=testing webpack",
"build": "npm run pre-build; cross-env NODE_ENV=production BROWSER=firefox CHANNEL=stable webpack --hide-modules",
"build-all": "mkdir -p ./build/old; npm run pre-build; rm ./dist-zip/uw-amo-source.zip; mv -f ./dist-zip/*.zip ./build/old; npm run build; node scripts/build-zip.js ff; npm run build-chrome; node scripts/build-zip.js chrome; ./scripts/prepare-amo-source.sh",
"build-chrome": "cross-env NODE_ENV=production BROWSER=chrome CHANNEL=stable webpack --hide-modules",
"build-chrome:dev": "cross-env NODE_ENV=development BROWSER=chrome webpack --hide-modules",
"build-edge": "cross-env NODE_ENV=production BROWSER=edge CHANNEL=stable webpack --hide-modules",
"build-nightly": "cross-env NODE_ENV=development BROWSER=firefox CHANNEL=nightly webpack --hide-modules",
"build-nightly-chrome": "cross-env NODE_ENV=development BROWSER=chrome CHANNEL=nightly webpack --hide-modules",
"build-testing": "cross-env NODE_ENV=development BROWSER=firefox CHANNEL=testing webpack --hide-modules",
"build-testing-chrome": "cross-env NODE_ENV=development BROWSER=chrome CHANNEL=testing webpack --hide-modules",
"build-zip": "node scripts/build-zip.js",
"build:dev": "webpack",
"dev": "cross-env NODE_ENV=development CHANNEL=dev concurrently \"cross-env BROWSER=firefox npm run build:dev -- --watch\" \"cross-env BROWSER=chrome npm run build:dev -- --watch\" \"cross-env BROWSER=edge npm run build:dev -- --watch\"",
"dev:windows": "cross-env NODE_ENV=development CHANNEL=dev concurrently \"cross-env BROWSER=firefox npm run build:dev -- -w --watch-poll\" \"cross-env BROWSER=chrome npm run build:dev -- -w --watch-poll\" \"cross-env BROWSER=edge npm run build:dev -- -w --watch-poll\"",
"dev-ff": "rm -rf ./dist-ff && cross-env NODE_ENV=development CHANNEL=dev BROWSER=firefox npm run build:dev -- --watch",
"dev-chrome": "rm -rf ./dist-chrome && cross-env NODE_ENV=development CHANNEL=dev BROWSER=chrome npm run build:dev -- --watch",
"dev-edge": "rm -rf ./dist-edge && cross-env NODE_ENV=development CHANNEL=dev BROWSER=edge npm run build:dev -- --watch",
"pre-build": "rm -rf ./dist-ff; rm -rf ./dist-chrome; rm -rf ./dist-edge; rm -rf ./node_modules; npm ci",
"start": "npm run dev",
"start:windows": "npm run dev:windows"
"build:dev": "webpack --hide-modules",
"dev": "cross-env NODE_ENV=development CHANNEL=dev concurrently \"cross-env BROWSER=firefox npm run build:dev -- --watch\" \"cross-env BROWSER=chrome npm run build:dev -- --watch\"",
"pre-build": "rm -rf ./dist-ff; rm -rf ./dist-chrome; rm -rf ./node_modules; npm ci",
"start": "npm run dev"
},
"dependencies": {
"@babel/plugin-proposal-class-properties": "^7.18.6",
"@mdi/font": "^7.4.47",
"@mdi/js": "^7.4.47",
"@types/resize-observer-browser": "^0.1.11",
"concurrently": "^9.2.1",
"fs-extra": "^11.3.2",
"gl-matrix": "^3.4.4",
"json-cyclic": "1.0.2",
"json-difference": "^1.16.1",
"lodash": "^4.17.21",
"mdi-vue": "^3.0.13",
"typescript": "^5.9.3",
"vanilla-jsoneditor": "^3.10.0",
"vue": "^3.5.25",
"vue-style-loader": "^4.1.3",
"vuex": "^4.1.0",
"vuex-webextensions": "^1.3.3",
"webextension-polyfill": "^0.12.0"
"@babel/plugin-proposal-class-properties": "^7.12.1",
"@types/core-js": "^2.5.3",
"@types/es6-promise": "^3.3.0",
"@vue/cli": "^4.5.9",
"bootstrap": "^4.5.3",
"bootstrap-icons": "^1.1.0",
"bootstrap-icons-vue": "^0.3.0",
"bootstrap-vue": "^2.20.1",
"concurrently": "^5.2.0",
"fs-extra": "^7.0.1",
"json-cyclic": "0.0.3",
"vue": "^3.0.0-beta.1",
"vuex": "^4.0.0-alpha.1",
"vuex-webextensions": "^1.3.0"
},
"devDependencies": {
"@babel/core": "^7.28.5",
"@babel/preset-env": "^7.28.5",
"@tailwindcss/postcss": "^4.1.17",
"@types/chrome": "0.1.31",
"@types/core-js": "^2.5.8",
"@types/es6-promise": "^3.3.2",
"@types/firefox": "0.0.34",
"@types/lodash": "^4.17.21",
"@types/node": "^24.10.1",
"@vue/cli": "^5.0.9",
"@vue/cli-plugin-typescript": "^5.0.9",
"@vue/compiler-sfc": "^3.5.25",
"archiver": "^7.0.1",
"babel-loader": "^10.0.0",
"babel-preset-es2020": "^1.0.2",
"copy-webpack-plugin": "^13.0.1",
"cross-env": "^10.1.0",
"css-loader": "^7.1.2",
"ejs": "^3.1.10",
"file-loader": "^6.2.0",
"mini-css-extract-plugin": "^2.9.4",
"postcss": "^8.5.6",
"postcss-import": "^16.1.1",
"postcss-nested": "^7.0.2",
"postcss-preset-env": "^10.4.0",
"postcss-strip-inline-comments": "^0.1.5",
"raw-loader": "^4.0.2",
"resolve-url-loader": "^5.0.0",
"tailwindcss": "^4.1.17",
"ts-loader": "^9.5.4",
"tsconfig-paths": "^4.2.0",
"@babel/core": "^7.10.5",
"@babel/plugin-proposal-optional-chaining": "^7.10.4",
"@babel/preset-env": "^7.10.4",
"@vue/compiler-sfc": "^3.0.3",
"archiver": "^3.0.0",
"babel-loader": "^8.1.0",
"copy-webpack-plugin": "^4.5.3",
"cross-env": "^5.2.0",
"css-loader": "^0.28.11",
"ejs": "^2.7.4",
"file-loader": "^1.1.11",
"mini-css-extract-plugin": "^0.4.4",
"node-sass": "^4.14.1",
"sass-loader": "^7.1.0",
"vue-cli-plugin-vue-next": "~0.1.4",
"vue-loader": "^17.4.2",
"vue-tsc": "^3.1.5",
"web-ext-types": "^3.2.1",
"webpack": "^5.103.0",
"webpack-cli": "^6.0.1",
"webpack-shell-plugin": "^0.5.0",
"webpack-shell-plugin-next": "^2.3.3"
"vue-loader": "^16.0.0",
"web-ext-types": "^2.1.0",
"webextension-polyfill": "^0.6.0",
"webpack": "^4.44.0",
"webpack-chrome-extension-reloader": "^0.8.3",
"webpack-cli": "^3.3.12",
"webpack-shell-plugin": "^0.5.0"
}
}

View File

@ -1,10 +0,0 @@
const path = require('path')
module.exports = {
'@src': path.resolve(__dirname, 'src'),
'@ui': path.resolve(__dirname, 'src/ui'),
'@components': path.resolve(__dirname, 'src/ui/components'),
'@res': path.resolve(__dirname, 'src/ui/res'),
'@': path.resolve(__dirname, 'src'),
};

View File

@ -1,24 +0,0 @@
const path = require('path');
const aliases = require('./paths.config');
module.exports = {
plugins: [
require('postcss-import')({
resolve(id) {
for (const alias in aliases) {
if (id === alias || id.startsWith(alias + '/')) {
return path.resolve(
aliases[alias],
id.slice(alias.length + 1)
)
}
}
return id
},
}),
require('@tailwindcss/postcss'),
require('postcss-nested'),
require('autoprefixer'),
],
};

View File

@ -1,58 +0,0 @@
#!/bin/bash
# NOTE: this script needs to be run with the npm run build-all
# command from the root directory of the project. Running it in
# any other way probably isn't going to work.
# pre-build steps:
echo "—————————————————————————————————————————————————————";
echo " BUILDING ALL ULTRAWIDIFY VERSIONS";
echo "—————————————————————————————————————————————————————";
mkdir -p ./build/old
npm run pre-build
echo "::"
echo "::"
echo "::"
echo ":: Pre-build complete. Moving old builds from dist-zip ..."
rm ./dist-zip/uw-amo-source.zip
mv -f ./dist-zip/*.zip ./build/old
echo "::"
echo "::"
echo "::"
echo ":: dist-zip is clean. Starting builds ..."
# lets force raise ram limit, but the improper way
# export NODE_OPTIONS=--max_old_space_size=4096
# build the version for each browser and create a zip afterwards
# step 1: define build functions
#function buildFF {
npm run build
node scripts/build-zip.js ff
#}
#function buildChrome {
npm run build-chrome
node scripts/build-zip.js chrome
#}
#function buildEdge {
npm run build-edge
node scripts/build-zip.js edge
#}
# step 2: execute them all at once
# buildFF &
# buildChrome &
# buildEdge &
# wait < <(jobs -p)
# prepare AMO source
# source code needs to be prepared AFTER
# the code has been built, to ensure that
# package-lock.json remains unchanged
./scripts/prepare-amo-source.sh

View File

@ -37,9 +37,6 @@ if [ ! -z "$GIT_COMMIT" ] ; then
fi
fi
# let's raise RAM limit for npm command globally
NODE_OPTIONS=--max_old_space_size=4096
npm ci
rm -rf ./dist-zip || true # no big deal if ./dist-zip doesn't exist
@ -49,7 +46,7 @@ mkdir dist-zip # create it back
# build firefox
#
npm run "${BUILD_SCRIPT}"
node --max-old-space-size=2048 scripts/build-zip.js ff nightly
node scripts/build-zip.js ff nightly
# if [ ! -z "${AMO_API_KEY}" ] ; then
# if [ ! -z "${AMO_API_SECRET}" ] ; then
# web-ext sign --source-dir ./dist --api-key "${AMO_API_KEY}" --api-secret "${AMO_API_SECRET}"
@ -60,18 +57,12 @@ node --max-old-space-size=2048 scripts/build-zip.js ff nightly
# build chrome
#
npm run "${BUILD_SCRIPT}-chrome"
node --max-old-space-size=2048 scripts/build-zip.js chrome nightly
node scripts/build-zip.js chrome nightly
#
#./scripts/build-crx.sh
#
#
# build edge
#
npm run "${BUILD_SCRIPT}-edge"
node --max-old-space-size=2048 scripts/build-zip.js chrome nightly
######################################
# UPLOAD TO WEB SERVER
######################################
@ -85,6 +76,7 @@ echo "Uploading to server ..."
scp -i ~/.ssh/id_rsa -r ./dist-zip/* "ultrawidify-uploader@${RELEASE_SERVER}:${RELEASE_DIRECTORY}${BUILD_CHANNEL_DIRECTORY}"
######################################
# Build finished message
######################################

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
# using the crx name we want (same name as zip, except different extension)
#
#
# 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 "exiting dist-zip"
cd ..
cd ..

View File

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

View File

@ -5,7 +5,8 @@ const fs = require('fs');
const BUNDLE_DIR = path.join(__dirname, `../dist-${process.env.BROWSER === 'firefox' ? 'ff' : process.env.BROWSER}`);
const bundles = [
// 'csui/csui-popup.js',
'popup/popup.js',
'options/options.js',
];
const evalRegexForProduction = /;([a-z])=function\(\){return this}\(\);try{\1=\1\|\|Function\("return this"\)\(\)\|\|\(0,eval\)\("this"\)}catch\(t\){"object"==typeof window&&\(\1=window\)}/g;

View File

@ -0,0 +1,249 @@
<template>
<div class="action">
<div class="flex-row action-name-cmd-container">
<div class="">
</div>
<div class="flex action-name">
<span v-if="action.cmd && action.cmd.length > 1 || action.cmd[0].action === 'set-ar' && action.userAdded || (action.cmd[0].arg === AspectRatio.Fixed)" class="icon red" @click="removeAction()">🗙</span>
<span v-else class="icon transparent">🗙</span> &nbsp; &nbsp;
<span class="icon" @click="editAction()">🖉</span> &nbsp; &nbsp;
{{action.name}}
</div>
</div>
<div class="command-details">
<div class="flex flex-column cmd-container cd-pad">
<div class="flex bold">
Command:
</div>
<div class="flex cmdlist">
{{parseCommand(action.cmd)}}
</div>
</div>
<!-- SCOPES -->
<div class="flex flex-column scopes-container cd-pad">
<div class="flex bold">Popup tabs:</div>
<!-- global scope -->
<div v-if="action.scopes.global"
class="flex flex-row scope-row"
>
<div class="flex scope-scope">
Global:
</div>
<div class="flex scope-row-label scope-visible">
Visible?&nbsp;<span class="scope-row-highlight">{{action.scopes.global.show ? 'Yes' : 'No'}}</span>
</div>
<div v-if="action.scopes.global.show"
class="flex scope-row-label scope-button-label"
>
Button label?&nbsp;
<span class="scope-row-highlight">
{{action.scopes.global.label ? action.scopes.global.label : (action.label ? action.label : '')}}
</span>
</div>
<div class="flex scope-row-label">
Keyboard shortcut:&nbsp;
<span class="scope-row-highlight">
{{action.scopes.global.shortcut ? parseActionShortcut(action.scopes.global.shortcut[0]) : 'None'}}
</span>
</div>
</div>
<!-- site scope -->
<div v-if="action.scopes.site"
class="flex flex-row scope-row"
>
<div class="flex scope-scope">
Site:
</div>
<div class="flex scope-row-label scope-visible">
Visible?&nbsp;<span class="scope-row-highlight">{{action.scopes.site.show ? 'Yes' : 'No'}}</span>
</div>
<div v-if="action.scopes.site.show"
class="flex scope-row-label scope-button-label"
>
Button label?&nbsp;
<span class="scope-row-highlight">
{{action.scopes.site.label ? action.scopes.site.label : (action.label ? action.label : '')}}
</span>
</div>
<div class="flex scope-row-label">
Keyboard shortcut:&nbsp;
<span class="scope-row-highlight">
{{action.scopes.site.shortcut ? parseActionShortcut(action.scopes.site.shortcut[0]) : 'None'}}
</span>
</div>
</div>
<!-- page scope -->
<div v-if="action.scopes.page"
class="flex flex-row scope-row"
>
<div class="flex scope-scope">
Page:
</div>
<div class="flex scope-row-label scope-visible">
Visible?&nbsp;<span class="scope-row-highlight">{{action.scopes.page.show ? 'Yes' : 'No'}}</span>
</div>
<div v-if="action.scopes.page.show"
class="flex scope-row-label scope-button-label"
>
Button label?&nbsp;
<span class="scope-row-highlight">
{{action.scopes.page.label ? action.scopes.page.label : (action.label ? action.label : '')}}
</span>
</div>
<div class="flex scope-row-label">
Keyboard shortcut:&nbsp;
<span class="scope-row-highlight">
{{action.scopes.page.shortcut ? parseActionShortcut(action.scopes.page.shortcut[0]) : 'None'}}
</span>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import Stretch from '../enums/stretch.enum';
import AspectRatio from '../enums/aspect-ratio.enum';
import KeyboardShortcutParser from '../js/KeyboardShortcutParser';
export default {
data () {
return {
Stretch: Stretch,
AspectRatio: AspectRatio,
}
},
created () {
},
props: {
action: Object,
index: Number,
},
methods: {
parseActionShortcut(shortcut) {
return KeyboardShortcutParser.parseShortcut(shortcut);
},
parseCommand(cmd) {
let cmdstring = '';
for (const c of cmd) {
cmdstring += `${c.action} ${c.arg}${c.persistent ? ' persistent' : ''}; `;
}
return cmdstring;
},
editAction() {
this.$emit('edit');
},
removeAction() {
this.$emit('remove');
}
}
}
</script>
<style lang="scss" scoped>
@import '../../res/css/colors.scss';
.uw-ultrawidify-container-root {
.action {
cursor: pointer;
position: relative;
box-sizing: border-box;
}
.action .command-details {
height: 0px;
max-height: 0px;
transition: max-height 0.5s ease;
overflow: hidden;
transition: height 0.5s ease;
position: absolute;
top: 0;
right: 0;
width: 50%;
}
.action:hover .command-details {
height: auto;
max-height: 200px;
transition: max-height 0.5s ease;
}
.command-details {
position: absolute;
top: 0;
right: 0;
width: 50%;
}
.action-name-cmd-container, .p1rem {
padding: 1rem;
}
.cd-pad {
padding: 0.5em;
}
.action-name {
font-size: 1.5rem;
font-weight: 300;
color: $text-normal;
width: 50%;
}
.action-name:hover, .action:hover .action-name {
color: lighten($primary-color, 20%);
}
.red {
color: $primary-color !important;
}
.cmd-container {
width: 13.37rem;
}
.cmdlist {
font-family: 'Overpass Mono';
font-size: 0.9rem;
color: $text-dim;
}
.bold {
font-weight: 600;
}
.scope-scope {
width: 5rem;
text-align: right !important;
color: $secondary-color;
}
.scope-visible {
width: 7rem;
}
.scope-button-label {
width: 16rem;
}
.scope-row-label {
color: $text-dark;
}
.scope-row-highlight {
color: $text-normal;
}
.transparent {
opacity: 0;
}
}
</style>

View File

@ -0,0 +1,76 @@
<template>
<tr>
<td class="cmd monospace">{{parseCommand(action.cmd)}}</td>
<td class="">{{action.label ? action.label : ""}}</td>
<td class="shortcut center-text">{{parseActionShortcut(action)}}</td>
<td class="center-text">
<div class="checkbox-container">
<span class="checkbox"
:class="{'checkbox-checked': (action.shortcut && action.shortcut.length &&
(action.shortcut[0].onMouseMove || action.shortcut[0].onClick ||
action.shortcut[0].onScroll))}"
></span>
</div>
</td>
<td class="center-text">
<div class="checkbox-container">
<span class="checkbox"
:class="{'checkbox-checked': action.popup_global}"
>
</span>
</div>
</td>
<td class="center-text">
<div class="checkbox-container">
<span class="checkbox"
:class="{'checkbox-checked': action.popup_site}"
></span>
</div>
</td>
<td class="center-text">
<div class="checkbox-container">
<span class="checkbox"
:class="{'checkbox-checked': action.popup}"
></span>
</div>
</td>
<td class="center-text">
<div class="checkbox-container">
<span class="checkbox"
:class="{'checkbox-checked': action.ui}"
></span>
</div>
</td>
<td>{{action.ui_path ? action.ui_path : ""}}</td>
</tr>
</template>
<script>
import Stretch from '../enums/stretch.enum';
import KeyboardShortcutParser from '../js/KeyboardShortcutParser'
export default {
data () {
return {
Stretch: Stretch
}
},
created () {
},
props: {
action: Object
},
methods: {
parseActionShortcut(action) {
return KeyboardShortcutParser.parseShortcut(action.shortcut[0]);
},
parseCommand(cmd) {
let cmdstring = '';
for(const c of cmd) {
cmdstring += `${c.action} ${c.arg}${c.persistent ? ' persistent' : ''}; `;
}
return cmdstring;
}
}
}
</script>

View File

@ -0,0 +1,17 @@
<template>
<div class="button center-text flex"
:class="{'setting-selected': selected, 'w24': fixedWidth, 'flex-auto': !fixedWidth }"
>
{{label}}
</div>
</template>
<script>
export default {
props: {
fixedWidth: Boolean,
selected: Boolean,
label: String,
}
}
</script>

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,83 @@
<template>
<div class="flex flex-column json-level-indent">
<div class="flex flex-row" @click="expanded_internal = !expanded_internal">
<div v-if="value_internal.key" class="item-key">
"{{value_internal.key}}" <b>:</b>
<span v-if="!expanded_internal"><b> [</b> ... <b>]</b>,</span>
<template v-else><b>[</b></template>
</div>
</div>
<div v-for="(row, key) of value_internal.value"
:key="key"
>
<JsonArray v-if="Array.isArray(row)"
:value="row"
:ignoreKeys="ignoreKeys"
@change="changeItem(rowKey, $event)"
>
</JsonArray>
<JsonObject v-else-if="typeof row === 'object' && row !== null"
:value="row"
:label="rowKey"
:ignoreKeys="ignoreKeys"
@change="changeItem(rowKey, $event)"
>
</JsonObject>
<JsonElement v-else
:value="row"
:label="rowKey"
@change="changeItem(rowKey, $event)"
>
</JsonElement>
</div>
<div v-if="expanded_internal"><b>],</b></div>
</div>
</template>
<script>
import JsonObject from './JsonObject';
import JsonElement from './JsonElement';
export default {
name: 'JsonArray',
props: [
'value',
'expanded',
'ignoreKeys', // this prop is passthrough for JsonArray
],
components: {
JsonObject,
JsonElement,
},
data() {
return {
value_internal: undefined,
expanded_internal: true,
}
},
created() {
this.value_internal = this.value;
},
watch: {
value: function(val) {
this.value_internal = val;
},
expanded: function(val) {
if (val !== undefined && val !== null) {
this.expanded_internal = !!val;
}
}
},
methods: {
changeItem(key, value) {
this.value_internal[key] = value;
this.$emit('change', this.value_internal);
}
}
}
</script>
<style lang="scss" scoped src="./json.scss">
</style>
<style lang="scss" scoped src="../../../res/css/flex.scss">
</style>

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-column json-level-indent">
<div class="flex flex-row" @click="expanded_internal = !expanded_internal">
<div class="item-key-line">
<template v-if="label">
<b>
<span class="item-key">"{{label}}"</span>
:
</b>
</template>
<span v-if="!expanded_internal"><b> {</b> ... <b>}</b>,</span>
<template v-else><b>{</b></template>
</div>
</div>
<template v-if="expanded_internal">
<div v-for="(row, rowKey) of value_internal"
:key="rowKey"
>
<template v-if="(ignoreKeys || {})[rowKey] !== true">
<JsonArray v-if="Array.isArray(row)"
:value="row"
:ignoreKeys="(ignoreKeys || {})[rowKey]"
@change="changeItem(rowKey, $event)"
>
</JsonArray>
<JsonObject v-else-if="typeof row === 'object' && row !== null"
:value="row"
:label="rowKey"
:ignoreKeys="(ignoreKeys || {})[rowKey]"
@change="changeItem(rowKey, $event)"
>
</JsonObject>
<JsonElement v-else
:value="row"
:label="rowKey"
@change="changeItem(rowKey, $event)"
>
</JsonElement>
</template>
</div>
<div><b>},</b></div>
</template>
</div>
</template>
<script>
import JsonArray from './JsonArray';
import JsonElement from './JsonElement';
export default {
name: 'JsonObject',
props: [
'value',
'label',
'expanded',
'ignoreKeys',
],
components: {
JsonArray,
JsonElement,
},
data() {
return {
value_internal: undefined,
expanded_internal: true,
}
},
created() {
this.value_internal = this.value;
},
watch: {
value: function(val) {
this.value_internal = val;
},
expanded: function(val) {
if (val !== undefined && val !== null) {
this.expanded_internal = !!val;
}
}
},
methods: {
changeItem(key, value) {
this.value_internal[key] = value;
this.$emit('change', this.value_internal);
}
}
}
</script>
<style lang="scss" scoped src="./json.scss">
</style>

View File

@ -0,0 +1,25 @@
.uw-ultrawidify-container-root {
.json-level-indent {
padding-left: 2em !important;
font-family: 'Overpass Mono', monospace;
}
.item-key {
color: rgb(255, 196, 148);
}
.item-key-boolean-false {
color: rgb(207, 149, 101)
}
.json-value-boolean-true {
color: rgb(150, 240, 198);
}
.json-value-boolean-false {
color: rgb(241, 21, 21);
}
.json-value-number {
color: rgb(121, 121, 238);
}
.json-value-string {
color: rgb(226, 175, 7);
}
}

View File

@ -0,0 +1,28 @@
<template>
<div class="root">
<template v-for="key in json"
>
<div class="element" :key="key">
<span class="json-key">"{{key}}"</span>:
<template v-if="Array.isArray(json[key])">
[<br/>
<div v-for="index of json[key]"
class="json-table-row"
:key="index"
>
</div>
]
</template>
</div>
</template>
</div>
</template>
<script>
export default {
name: "json-explorer",
props: {
json: Object
}
}
</script>

View File

@ -0,0 +1,55 @@
<template>
<div class="qsb flex flex-row flex-cross-center flex-center flex-nogrow"
:class="{
'id': selector.startsWith('#'),
'class': selector.startsWith('.'),
}">
<div class="flex mt2px">{{selector}}</div> <span class="flex closeButton" @click="remove">×</span>
</div>
</template>
<script>
export default {
props: {
selector: String,
},
methods: {
remove() {
this.$emit('remove');
}
}
}
</script>
<style lang="scss" scoped>
.uw-ultrawidify-container-root {
.mt2px {
margin-top: 7px;
}
.id {
border: 1px solid #d00;
background-color: rgba(216, 94, 24, 0.25)
}
.class {
border: 1px solid #33d;
background-color: rgba(69, 69, 255, 0.25)
}
.closeButton {
margin-top: 2px;
margin-left: 4px;
margin-right: 4px;
font-size: 1.5em;
color: #e00;
}
.closeButton:hover {
cursor: pointer;
color: #fa6;
}
.qsb {
cursor:default;
margin-left: 3px;
margin-right: 3px;
margin-bottom: 2px;
}
}
</style>

View File

@ -0,0 +1,87 @@
<template>
<div class="flex flex-column">
<div v-if="!editing && !adding" class="flex flex-row">
<div class="">
<b>Query selector:</b> {{qs.string}}<br/>
<b>Additional CSS:</b> {{qs.css || 'no style rules'}}
</div>
<div class="flex flex-column flex-nogrow">
<a @click="editing = true">Edit</a>
<a @click="$emit('delete')">Delete</a>
</div>
</div>
<div v-else class="flex flex-row">
<div class="flex flex-column">
<div class="flex flex-row">
<div class="flex label-secondary form-label">
Query selector:
</div>
<div class="flex flex-input">
<input type="text"
v-model="qs.string"
/>
</div>
</div>
<div class="flex flex-row">
<div class="flex label-secondary form-label">
Additional CSS:
</div>
<div class="flex flex-input">
<input type="text"
v-model="qs.css"
/>
</div>
</div>
</div>
<div>
<a v-if="editing" @click="cancelEdit">Cancel</a>
<a v-if="adding" @click="clear">Clear</a>
<a @click="save">Save</a>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
qs: {string: '', css: ''},
editing: false,
}
},
props: {
querySelector: Object,
adding: {
type: Boolean,
required: false,
default: false
}
},
watch: {
querySelector(val) {
this.qs = JSON.parse(JSON.stringify(val));
}
},
methods: {
save() {
if (this.adding) {
this.$emit('create', this.qs);
this.qs = {string: '', css: ''};
} else {
this.emit$('update', this.qs)
}
},
cancelEdit() {
this.qs = JSON.parse(JSON.stringify(this.querySelector));
},
clear() {
this.qs = {string: '', css: ''};
}
}
}
</script>
<style>
</style>

View File

@ -0,0 +1,32 @@
<template>
<div class="flex flex-column flex-center">
<div class="flex flex-self-center">
{{label}}
</div>
<div class="flex dark flex-self-center">
<small>
{{shortcut ? `(${shortcut})` : ''}}
</small>
</div>
</div>
</template>
<script>
export default {
props: {
label: String,
shortcut: String
}
}
</script>
<style lang="scss" scoped>
.uw-ultrawidify-container-root {
.center-text {
text-align: center;
}
.dark {
opacity: 50%;
}
}
</style>

View File

@ -6,7 +6,7 @@ let Notifications = Object.freeze({
},
'AARD_DRM': {
icon: 'exclamation-triangle',
text: '<b>Autodetection may not be able to run on this video.</b> On sites that utilize DRM you will have to set aspect ratio manually.',
text: '<b>Autodetection cannot run on this video.</b> This usually happens when sites use DRM. You will have to set aspect ratio manually.',
timeout: 5000,
}
});

View File

@ -1,7 +0,0 @@
enum AntiGradientMode {
Disabled = 0,
Lax = 1,
Strict = 2
}
export default AntiGradientMode;

View File

@ -1,15 +0,0 @@
enum AspectRatioType {
Default = -3, // inherit from global settings
Cycle = -2,
Initial = -1, // page default
Reset = 0, // reset to initial
Automatic = 1, // we want to request automatic aspect ratio detection
FitWidth = 2, // legacy/dynamic = fit to width
FitHeight = 3, // legacy/dynamic = fit to height
Fixed = 4, // pre-determined aspect ratio
Manual = 5, // ratio achieved by zooming in/zooming out
AutomaticUpdate = 6, // set by Aard
Cover = 7, // replaces FitWidth and FitHeight
}
export default AspectRatioType;

View File

@ -1,9 +0,0 @@
enum CropModePersistence {
Default = -1,
Disabled = 0,
UntilPageReload = 1,
CurrentSession = 2,
Forever = 3,
}
export default CropModePersistence;

View File

@ -1,8 +0,0 @@
enum EmbeddedContentSettingsOverridePolicy {
Always = 2,
UseAsDefault = 1,
Default = 0,
Never = -1,
}
export default EmbeddedContentSettingsOverridePolicy;

View File

@ -1,9 +0,0 @@
enum ExtensionMode {
Disabled = -1,
Default = 0,
FullScreen = 1,
Theater = 2,
All = 3
}
export default ExtensionMode;

View File

@ -1,5 +0,0 @@
export enum InputHandlingMode {
Disabled = -1,
Default = 0,
Enabled = 1, // enable, but don't override site shortcuts
}

View File

@ -1,10 +0,0 @@
enum LegacyExtensionMode {
AutoDisabled = -2,
Disabled = -1,
Default = 0,
Whitelist = 1,
Basic = 2,
Enabled = 3,
};
export default LegacyExtensionMode;

View File

@ -1,5 +0,0 @@
export enum PlayerDetectionMode {
Auto = 0,
AncestorIndex = 1,
QuerySelectors = 2,
};

View File

@ -1,9 +0,0 @@
export enum SiteSupportLevel {
OfficialSupport = 'official',
CommunitySupport = 'community',
UserDefined = 'user-defined',
BetaSupport = 'testing',
OfficialBlacklist = 'officially-disabled',
Unknown = 'unknown',
UserModified = 'modified',
}

View File

@ -1,11 +0,0 @@
enum StretchType {
NoStretch = 0,
Basic = 1,
Hybrid = 2,
Conditional = 3,
Fixed = 4,
FixedSource = 5,
Default = -1
};
export default StretchType;

View File

@ -1,11 +0,0 @@
enum VideoAlignmentType {
Left = 0,
Center = 1,
Right = 2,
Top = 3,
Bottom = 4,
Default = -1,
Custom = -2,
};
export default VideoAlignmentType;

View File

@ -0,0 +1,7 @@
var AntiGradientMode = Object.freeze({
Disabled: 0,
Lax: 1,
Strict: 2
});
export default AntiGradientMode;

View File

@ -0,0 +1,11 @@
var AspectRatio = Object.freeze({
Initial: -1, // page default
Reset: 0, // reset to initial
Automatic: 1, // set by Aard
FitWidth: 2, // legacy/dynamic: fit to width
FitHeight: 3, // legacy/dynamic: fit to height
Fixed: 4, // pre-determined aspect ratio
Manual: 5, // ratio achieved by zooming in/zooming out
});
export default AspectRatio;

View File

@ -0,0 +1,9 @@
var CropModePersistence = Object.freeze({
Default: -1,
Disabled: 0,
UntilPageReload: 1,
CurrentSession: 2,
Forever: 3,
});
export default CropModePersistence;

View File

@ -0,0 +1,19 @@
if (process.env.CHANNEL !== 'stable') {
console.info('Loading ExtensionMode');
}
var ExtensionMode = Object.freeze({
AutoDisabled: -2,
Disabled: -1,
Default: 0,
Whitelist: 1,
Basic: 2,
Enabled: 3,
});
if (process.env.CHANNEL !== 'stable') {
console.info('Loaded ExtensionMode');
}
export default ExtensionMode;

View File

@ -0,0 +1,11 @@
var Stretch = Object.freeze({
NoStretch: 0,
Basic: 1,
Hybrid: 2,
Conditional: 3,
Fixed: 4,
FixedSource: 5,
Default: -1
});
export default Stretch;

View File

@ -0,0 +1,8 @@
var VideoAlignment = Object.freeze({
Left: 0,
Center: 1,
Right: 2,
Default: -1
});
export default VideoAlignment;

View File

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

View File

@ -1,31 +0,0 @@
import { CommandInterface, InPlayerUIOptions } from '@src/common/interfaces/SettingsInterface';
export interface MenuItemBaseConfig {
label?: string;
subitems?: MenuItemConfig[];
command?: CommandInterface,
action?: () => void;
customHTML?: HTMLElement | string;
customId?: string;
customClassList?: string;
}
export type MenuItemConfig = MenuItemBaseConfig & ({label: string} | {customHTML: HTMLElement | string});
export enum MenuPosition {
TopLeft = 'top-left',
Left = 'left',
BottomLeft = 'bottom-left',
Top = 'top',
Bottom = 'bottom',
TopRight = 'top-right',
Right = 'right',
BottomRight = 'bottom-right',
}
export interface MenuConfig {
isGlobal?: boolean;
ui: InPlayerUIOptions;
options?: {forceShow?: boolean};
items: MenuItemConfig[];
}

View File

@ -1,43 +0,0 @@
import { CommsOrigin } from '@/ext/module/comms/CommsClient';
import type { Runtime } from 'chrome';
export interface EventBusCommand {
isGlobal?: boolean,
source?: any,
function: (commandData: any, context?: EventBusContext) => void | Promise<void>
}
export interface EventBusMessage<T = unknown> {
command: string;
config?: T;
context?: EventBusContext;
}
export interface EventBusContext {
stopPropagation?: boolean;
origin?: CommsOrigin;
frameUrl?: string;
// tab?: number;
// frame?: number;
// port?: string;
visitedBusses?: string[];
commandId?: string;
comms?: {
forwardTo?: 'all' | 'active' | 'popup' | 'contentScript' | 'all-frames';
sender?: Runtime.MessageSender;
port?: Runtime.Port;
// frame?: any;
sourceFrame?: {
tabId: number;
frameId: number;
};
};
borderCrossings?: {
commsServer?: boolean,
iframe?: boolean,
};
}

View File

@ -1,8 +0,0 @@
import { ExtensionEnvironment } from '@src/common/interfaces/SettingsInterface';
export interface HostInfo {
host: string,
hasVideo: boolean,
minEnvironment: ExtensionEnvironment,
maxEnvironment: ExtensionEnvironment
};

View File

@ -1,6 +0,0 @@
import { EventBusMessage } from '@src/common/interfaces/EventBusMessage.interface';
export interface IframeTunnelPayload {
action: string,
payload: EventBusMessage,
}

View File

@ -1,19 +0,0 @@
import VideoAlignmentType from '@src/common/enums/VideoAlignmentType.enum';
import { Ar } from '@src/common/interfaces/ArInterface';
import { Stretch } from '@src/common/interfaces/StretchInterface';
export interface ScalingParamsBroadcast {
effectiveZoom: {
x: number,
y: number
},
videoAlignment: {
x: VideoAlignmentType,
y: VideoAlignmentType,
xPos?: number,
yPos?: number,
}
lastAr: Ar,
manualZoom: boolean,
stretch: Stretch,
}

View File

@ -1,525 +0,0 @@
import { AardPollingOptions } from '@src/ext/module/aard/enums/aard-polling-options.enum'
import { AardSubtitleCropMode } from '@src/ext/module/aard/enums/aard-subtitle-crop-mode.enum'
import { SiteSupportLevel } from '@src/common/enums/SiteSupportLevel.enum';
import AspectRatioType from '@src/common/enums/AspectRatioType.enum'
import CropModePersistence from '@src/common/enums/CropModePersistence.enum'
import EmbeddedContentSettingsOverridePolicy from '@src/common/enums/EmbeddedContentSettingsOverridePolicy.enum'
import ExtensionMode from '@src/common/enums/ExtensionMode.enum'
import StretchType from '@src/common/enums/StretchType.enum'
import VideoAlignmentType from '@src/common/enums/VideoAlignmentType.enum'
import { PlayerDetectionMode } from '@src/common/enums/PlayerDetectionMode.enum';
import { InputHandlingMode } from '@src/common/enums/InputHandlingMode.enum';
import { MenuPosition } from '@src/common/interfaces/ClientUiMenu.interface';
export enum ExtensionEnvironment {
Normal = ExtensionMode.All,
Theater = ExtensionMode.Theater,
Fullscreen = ExtensionMode.FullScreen,
}
export interface DevUiConfig {
aardDebugOverlay: {
showOnStartup: boolean,
showDetectionDetails: boolean,
}
}
export interface KeyboardShortcutInterface {
key?: string,
code?: string,
ctrlKey?: boolean,
metaKey?: boolean,
altKey?: boolean,
shiftKey?: boolean,
onKeyUp?: boolean,
onKeyDown?: boolean,
onMouseMove?: boolean,
}
interface ActionScopeInterface {
show: boolean,
label?: string, // example override, takes precedence over default label
shortcut?: KeyboardShortcutInterface[],
}
interface RestrictionsSettings {
disableOnSmallPlayers?: boolean; // Whether ultrawidify should disable itself when the player is small
minAllowedWidth?: number; // if player is less than this many px wide, ultrawidify will disable itself
minAllowedHeight?: number; // if player is less than this many px tall, ultrawidify will disable itself
onlyAllowInFullscreen?: boolean; // if enabled, ultrawidify will be disabled when not in full screen regardless of what previous two options say
onlyAllowAutodetectionInFullScreen?: boolean; // if enabled, autodetection will only start once in full screen
}
export interface CommandInterface {
action: string,
label: string,
comment?: string,
arguments?: any,
shortcut?: KeyboardShortcutInterface,
internalOnly?: boolean,
actionId?: string,
}
export type SettingsReloadComponent = 'PlayerData' | 'VideoData';
export type SettingsReloadFlags = true | SettingsReloadComponent;
export interface AardSubtitleScanOptions {
subtitleCropMode: AardSubtitleCropMode,
resumeAfter: number, // resume after X ms of no subtitle
scanSpacing: number, // repeat subtitle scan every _n_ lines
scanMargin: number, // % of pixels (from edge to start area) that we skip while scanning.
// While technically anything between 0 and 0.5 is valid, the value
// should be somewhere between 0.1-0.3 in order for subtitle scan to
// work properly.
maxValidLetter: number, // if letter is longer than this, something's off.
subtitleSubpixelThresholdOn: number,
subtitleSubpixelThresholdOff: number,
minDetections: number,
minImageLineDetections: number,
maxPotentialSubtitleMisalignment: number, // how many pixels off-center can "potential subtitle" be
refiningScanSpacing: number, // must be base-2
refiningScanInitialIterations: number,
}
export interface AardSettings {
aardType: 'webgl' | 'legacy' | 'auto';
useLegacy: boolean,
earlyStopOptions: {
stopAfterFirstDetection: boolean;
stopAfterTimeout: boolean;
stopTimeout: number;
},
polling: {
runInBackgroundTabs: AardPollingOptions;
runOnSmallVideos: AardPollingOptions;
}
disabledReason: string, // if automatic aspect ratio has been disabled, show reason
allowedMisaligned: number, // top and bottom letterbox thickness can differ by this much.
// Any more and we don't adjust ar.
allowedArVariance: number, // amount by which old ar can differ from the new (1 = 100%)
timers: { // autodetection frequency
playing: number, // while playing
playingReduced: number, // while video/player element has insufficient size
paused: number, // while paused
error: number, // after error
minimumTimeout: number,
tickrate: number, // 1 tick every this many milliseconds
},
subtitles: AardSubtitleScanOptions,
autoDisable: { // settings for automatically disabling the extension
onFirstChange: boolean,
ifNotChanged: boolean,
ifNotChangedTimeout: number,
ifSubtitles: boolean;
},
canvasDimensions: {
blackframeCanvas: { // smaller than sample canvas, blackframe canvas is used to recon for black frames
// it's not used to detect aspect ratio by itself, so it can be tiny af
width: number,
height: number,
},
sampleCanvas: { // size of image sample for detecting aspect ratio. Bigger size means more accurate results,
// at the expense of performance
width: number,
height: number,
},
},
blackLevels: {
defaultBlack: number, // By default, pixels darker than this are considered black.
// (If detection algorithm detects darker blacks, black is considered darkest detected pixel)
blackTolerance: number, // If pixel is more than this much brighter than blackLevel, it's considered not black
// It is not considered a valid image detection if gradient detection is enabled
imageDelta: number, // When gradient detection is enabled, pixels this much brighter than black skip gradient detection
}
sampling: {
edgePosition: number; // % of width (max 0.33). Pixels up to this far away from either edge may contain logo.
staticCols: number, // we take a column at [0-n]/n-th parts along the width and sample it
randomCols: number, // we add this many randomly selected columns to the static columns
staticRows: number, // forms grid with staticSampleCols. Determined in the same way. For black frame checks,
},
// pls deprecate and move things used
edgeDetection: {
gradientThreshold: number, // if more than this percentage (0-1) is detected as gradient, we mark edge as gradient
gradientTestMinDelta: number, // if difference between test row and before row is MORE than this -> not gradient
gradientTestMinDeltaAfter: number, // if difference between test row and after row is LESS than this -> not gradient
gradientTestMaxDeltaAfter: number, // if difference between test row and after row is MORE than this -> not gradient
maxLetterboxOffset: number, // Upper and lower letterbox can be different by this many (% of height)
minValidImage: number, // if more than this % (0-1) of row is image, we confirm image regardless of other criteria except gradient
maxEdgeSegments: number, // if edge has more than this many segments, we consider it unreliable
minEdgeSegmentSize: number,
averageEdgeThreshold: number, // average edge must be this many px
},
letterboxOrientationScan: {
letterboxLimit: number, // how many non-black pixels we can detect before ruling out letterbox
pillarboxLimit: number, // how many non-black pixels we can detect before ruling out pillarbox
}
pillarTest: {
ignoreThinPillarsPx: number, // ignore pillars that are less than this many pixels thick.
allowMisaligned: number // left and right edge can vary this much (%)
},
textLineTest: {
nonTextPulse: number, // if a single continuous pulse has this many non-black pixels, we aren't dealing
// with text. This value is relative to canvas width (%)
pulsesToConfirm: number, // this is a threshold to confirm we're seeing text.
pulsesToConfirmIfHalfBlack: number, // this is the threshold to confirm we're seeing text if longest black pulse
// is over 50% of the canvas width
testRowOffset: number // we test this % of height from detected edge
}
}
export interface AardLegacySettings {
aardType: 'webgl' | 'legacy' | 'auto';
earlyStopOptions: {
stopAfterFirstDetection: boolean;
stopAfterTimeout: boolean;
stopTimeout: number;
},
polling: {
runInBackgroundTabs: AardPollingOptions;
runOnSmallVideos: AardPollingOptions;
}
disabledReason: string, // if automatic aspect ratio has been disabled, show reason
allowedMisaligned: number, // top and bottom letterbox thickness can differ by this much.
// Any more and we don't adjust ar.
allowedArVariance: number, // amount by which old ar can differ from the new (1 = 100%)
timers: { // autodetection frequency
playing: number, // while playing
playingReduced: number, // while video/player element has insufficient size
paused: number, // while paused
error: number, // after error
minimumTimeout: number,
tickrate: number, // 1 tick every this many milliseconds
},
subtitles: AardSubtitleScanOptions,
autoDisable: { // settings for automatically disabling the extension
onFirstChange: boolean,
ifNotChanged: boolean,
ifNotChangedTimeout: number,
ifSubtitles: boolean;
},
canvasDimensions: {
blackframeCanvas: { // smaller than sample canvas, blackframe canvas is used to recon for black frames
// it's not used to detect aspect ratio by itself, so it can be tiny af
width: number,
height: number,
},
sampleCanvas: { // size of image sample for detecting aspect ratio. Bigger size means more accurate results,
// at the expense of performance
width: number,
height: number,
},
},
blackLevels: {
defaultBlack: number, // By default, pixels darker than this are considered black.
// (If detection algorithm detects darker blacks, black is considered darkest detected pixel)
blackTolerance: number, // If pixel is more than this much brighter than blackLevel, it's considered not black
// It is not considered a valid image detection if gradient detection is enabled
imageDelta: number, // When gradient detection is enabled, pixels this much brighter than black skip gradient detection
}
sampling: {
edgePosition: number; // % of width (max 0.33). Pixels up to this far away from either edge may contain logo.
staticCols: number, // we take a column at [0-n]/n-th parts along the width and sample it
randomCols: number, // we add this many randomly selected columns to the static columns
staticRows: number, // forms grid with staticSampleCols. Determined in the same way. For black frame checks,
},
// pls deprecate and move things used
edgeDetection: {
slopeTestWidth: number,
gradientTestSamples: number, // we check this many pixels below (or above) the suspected edge to check for gradient
gradientTestBlackThreshold: number, // if pixel in test sample is brighter than that, we aren't looking at gradient
gradientTestDeltaThreshold: number, // if delta between two adjacent pixels in gradient test exceeds this, it's not gradient
thresholds: {
edgeDetectionLimit: number, // during scanning of the edge, quit after edge gets detected at this many points
minQualitySingleEdge: number, // At least one of the detected must reach this quality
minQualitySecondEdge: number, // The other edge must reach this quality (must be smaller or equal to single edge quality)
}
gradientThreshold: number, // if more than this percentage (0-1) is detected as gradient, we mark edge as gradient
gradientTestMinDelta: number, // if difference between test row and before row is MORE than this -> not gradient
gradientTestMinDeltaAfter: number, // if difference between test row and after row is LESS than this -> not gradient
gradientTestMaxDeltaAfter: number, // if difference between test row and after row is MORE than this -> not gradient
maxLetterboxOffset: number, // Upper and lower letterbox can be different by this many (% of height)
// Previous iteration variables VVVV
sampleWidth: number, // we take a sample this wide for edge detection
detectionThreshold: number, // sample needs to have this many non-black pixels to be a valid edge
confirmationThreshold: number, //
singleSideConfirmationThreshold: number, // we need this much edges (out of all samples, not just edges) in order
// to confirm an edge in case there's no edges on top or bottom (other
// than logo, of course)
logoThreshold: number, // if edge candidate sits with count greater than this*all_samples, it can't be logo
// or watermark.
edgeTolerancePx?: number, // we check for black edge violation this far from detection point
edgeTolerancePercent?: number, // we check for black edge detection this % of height from detection point. unused
middleIgnoredArea: number, // we ignore this % of canvas height towards edges while detecting aspect ratios
minColsForSearch: number, // if we hit the edge of blackbars for all but this many columns (%-wise), we don't
// continue with search. It's pointless, because black edge is higher/lower than we
// are now. (NOTE: keep this less than 1 in case we implement logo detection)
edgeMismatchTolerancePx: number,// corners and center are considered equal if they differ by at most this many px
minValidImage: number, // if more than this % (0-1) of row is image, we confirm image regardless of other criteria except gradient
maxEdgeSegments: number, // if edge has more than this many segments, we consider it unreliable
minEdgeSegmentSize: number,
averageEdgeThreshold: number, // average edge must be this many px
},
letterboxOrientationScan: {
letterboxLimit: number, // how many non-black pixels we can detect before ruling out letterbox
pillarboxLimit: number, // how many non-black pixels we can detect before ruling out pillarbox
}
pillarTest: {
ignoreThinPillarsPx: number, // ignore pillars that are less than this many pixels thick.
allowMisaligned: number // left and right edge can vary this much (%)
},
textLineTest: {
nonTextPulse: number, // if a single continuous pulse has this many non-black pixels, we aren't dealing
// with text. This value is relative to canvas width (%)
pulsesToConfirm: number, // this is a threshold to confirm we're seeing text.
pulsesToConfirmIfHalfBlack: number, // this is the threshold to confirm we're seeing text if longest black pulse
// is over 50% of the canvas width
testRowOffset: number // we test this % of height from detected edge
}
}
interface DevSettings {
loadFromSnapshot: boolean,
}
export interface InPlayerUIOptions {
activatorAlignment: MenuPosition,
minEnabledWidth: number, // don't show UI if player is narrower than % of screen width
minEnabledHeight: number, // don't show UI if player is narrower than % of screen height
activation: 'player' | 'trigger-zone' | 'distance' | 'none', // what needs to be hovered in order for UI to be visible
activateWithCtrl: boolean,
activationDistance: number,
activationDistanceUnits: '%' | 'px',
activatorPadding: {x: number, y: number}
activatorPaddingUnit: {x: '%' | 'px', y: '%' | 'px'},
triggerZoneDimensions: { // how large the trigger zone is (relative to player size)
width: number
height: number,
offsetX: number, // fed to translateX(offsetX + '%'). Valid range [-100, 0]
offsetY: number // fed to translateY(offsetY + '%'). Valid range [-100, 100]
},
};
interface SettingsInterface {
_updateFlags?: {
requireReload?: SettingsReloadFlags,
forSite?: string
}
dev: DevSettings,
aardLegacy: AardLegacySettings,
aard: AardSettings,
ui: {
inPlayer: InPlayerUIOptions,
devMode?: boolean,
dev: DevUiConfig,
}
restrictions?: RestrictionsSettings;
crop: {
default: any;
},
stretch: {
default: any;
conditionalDifferencePercent: number // black bars less than this wide will trigger stretch
// if mode is set to '1'. 1.0=100%
},
kbm: { // TODO: we dont use this anymore
enabled: boolean, // if keyboard/mouse handler service will run
keyboardEnabled: boolean, // if keyboard shortcuts are processed
mouseEnabled: boolean, // if mouse movement is processed
}
zoom: {
minLogZoom: number,
maxLogZoom: number,
announceDebounce: number // we wait this long before announcing new zoom
},
miscSettings: {
mousePan: {
enabled: boolean
},
mousePanReverseMouse: boolean,
defaultAr?: any
},
resizer: {
setStyleString: {
maxRetries: number,
retryTimeout: number
}
},
pageInfo: {
timeouts: {
urlCheck: number,
rescan: number
}
},
pan?: any,
version?: string,
preventReload?: boolean,
lastModified?: Date,
// -----------------------------------------
// ::: MITIGATIONS :::
// -----------------------------------------
// Settings for browser bug workarounds.
mitigations?: {
zoomLimit?: {
enabled?: boolean,
fullscreenOnly?: boolean,
limit?: number,
}
}
// This object fulfills the same purpose as 'actions', but is written in less retarded and overly
// complicated way. Hopefully it'll be easier to maintain it that way.
commands?: {
crop?: CommandInterface[],
stretch?: CommandInterface[],
zoom?: CommandInterface[],
pan?: CommandInterface[],
internal?: CommandInterface[],
},
whatsNewChecked: boolean,
newFeatureTracker: any,
// -----------------------------------------
// ::: SITE CONFIGURATION :::
// -----------------------------------------
// Config for a given page:
//
// <hostname> : {
// status: <option> // should extension work on this site?
// arStatus: <option> // should we do autodetection on this site?
//
// defaultAr?: <ratio> // automatically apply this aspect ratio on this side. Use extension defaults if undefined.
// stretch? <stretch mode> // automatically stretch video on this site in this manner
// videoAlignment? <left|center|right>
//
// type: <official|community|user> // 'official' — blessed by Tam.
// // 'community' — blessed by reddit.
// // 'user' — user-defined (not here)
// override: <true|false> // override user settings for this site on update
// }
//
// Valid values for options:
//
// status, arStatus, statusEmbedded:
//
// * enabled — always allow, full
// * basic — allow, but only the basic version without playerData
// * default — allow if default is to allow, block if default is to block
// * disabled — never allow
//
sites: {
[x: string]: SiteSettingsInterface,
}
}
export interface SiteSettingsInterface {
notes?: string; // any special things related to this site.
enable: ExtensionMode;
enableAard: ExtensionMode;
enableKeyboard: InputHandlingMode;
enableUI: ExtensionMode;
applyToEmbeddedContent: EmbeddedContentSettingsOverridePolicy; // presumed to be 'Use as default' if not defined
overrideWhenEmbedded?: EmbeddedContentSettingsOverridePolicy;
autocreated?: boolean;
type?: SiteSupportLevel;
defaultType: SiteSupportLevel;
// must be defined in @global and @empty
persistCSA?: CropModePersistence, // CSA - crop, stretch, alignment
defaults?: { // must be defined in @global and @empty
crop?: {type: AspectRatioType, [x: string]: any},
stretch?: {type: StretchType, ratio?: number},
alignment?: {x: VideoAlignmentType, y: VideoAlignmentType},
}
cropModePersistence?: CropModePersistence;
stretchModePersistence?: CropModePersistence;
alignmentPersistence?: CropModePersistence;
playerAutoConfig?: PlayerAutoConfigInterface;
activeDOMConfig?: string;
DOMConfig?: { [x: string]: SiteDOMSettingsInterface };
// the following script are for extension caching and shouldn't be saved.
// if they _are_ saved, they will be overwritten
currentDOMConfig?: SiteDOMSettingsInterface;
// the following fields are for use with extension update script
override?: boolean; // whether settings for this site will be overwritten by extension upgrade script
}
export interface PlayerAutoConfigInterface {
modified: boolean;
initialIndex: number;
currentIndex: number;
}
export interface SiteDOMSettingsInterface {
type: SiteSupportLevel; // 'official' | 'community' | 'user-defined' | 'modified' | undefined;
elements?: {
player?: PlayerDOMSettingsInterface,
video?: PlayerDOMSettingsInterface,
};
customCss?: string;
periodicallyRefreshPlayerElement?: boolean;
// the following script are for extension caching and shouldn't be saved.
// if they _are_ saved, they will be overwritten
anchorElementIndex?: number;
anchorElement?: HTMLElement;
}
export interface PlayerDOMSettingsInterface {
detectionMode: PlayerDetectionMode,
allowAutoFallback: boolean,
ancestorIndex?: number,
querySelectors?: string,
}
export interface SiteDOMElementSettingsInterface {
manual?: boolean;
querySelectors?: string;
index?: number; // previously: useRelativeAncestor + videoAncestor
mode?: 'index' | 'qs';
}
export default SettingsInterface;

View File

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

View File

@ -2,16 +2,16 @@
* For some reason, Chrome really doesn't like when chrome.runtime
* methods are wrapped inside a ES6 proxy object. If 'port' is a
* ES6 Proxy of a Port object that `chrome.runtime.connect()` creates,
* then Chrome will do bullshits like `port.sendMessage` and
* then Chrome will do bullshits like `port.sendMessage` and
* `port.onMessage.addListener` crashing your Vue3 UI with bullshits
* excuses, e.g.
*
*
* | TypeError: Illegal invocation. Function must be called on
* | an object of type Port
*
*
* which is some grade A bullshit because Firefox can handle that just
* fine.
*
*
* There's two ways how I could handle this:
* * Find out how to get the original object from the proxy Vue3
* creates, which would take time and ruin my xmass holiday, or
@ -19,8 +19,8 @@
* the very real possibility that there's prolly a reason Chrome
* does things in its own very special(tm) way, as if it had one
* extra chromosome over Firefox.
*
* Easy choice, really.
*
* Easy chhoice, really.
*/
export class ChromeShittinessMitigations {
static port = null;
@ -30,4 +30,4 @@ export class ChromeShittinessMitigations {
}
}
export default ChromeShittinessMitigations;
export default ChromeShittinessMitigations;

View File

@ -1,7 +1,7 @@
class KeyboardShortcutParser {
static parseShortcut(keypress) {
let shortcutCombo = '';
if (keypress.ctrlKey) {
shortcutCombo += 'Ctrl + ';
}
@ -21,20 +21,6 @@ class KeyboardShortcutParser {
}
return shortcutCombo;
}
static generateShortcutFromKeypress(event) {
return {
ctrlKey: event.ctrlKey,
altKey: event.altKey,
shiftKey: event.shiftKey,
metaKey: event.metaKey,
code: event.code,
key: event.key,
keyup: true,
keydown: false,
type: event.type, // only needed for purposes of EditShortcutButton
}
}
}
export default KeyboardShortcutParser;
export default KeyboardShortcutParser;

View File

@ -1,3 +1,3 @@
export async function sleep(timeout) {
return new Promise( (resolve, reject) => setTimeout(() => resolve(null), timeout));
return new Promise( (resolve, reject) => setTimeout(() => resolve(), timeout));
}

View File

@ -0,0 +1,17 @@
<template>
<div>
<p><i><a href="https://www.youtube.com/watch?v=Mn3YEJTSYs8&feature=youtu.be&t=770" target='_blank'>...</a> but as is normal, you can't use a free extensions without the developers begging for money. It's the bi-daily beggathon here on ... wherever here is.</i>
</p>
<p>
Jokes and references few will get aside, developing this extension does take a decent amount of time, motivation, carefully calibrated quantities
of alcohol and enough coffee to bankrupt a small nation.
</p>
<p>If you want to buy me a beer or bankroll my caffeine addiction, you can do so by <a href="https://paypal.me/tamius">clicking here</a>. All donations are appreciated.
</p>
</div>
</template>
<script>
</script>

View File

@ -0,0 +1,37 @@
export default {
computed: {
scopeActions: function() {
return this.settings.active.actions.filter(x => {
if (! x.scopes) {
console.error('This action does not have a scope.', x);
return false;
}
return x.scopes[this.scope] && x.scopes[this.scope].show
}) || [];
},
extensionActions: function(){
return this.scopeActions.filter(x => x.cmd.length === 1 && x.cmd[0].action === 'set-extension-mode') || [];
},
aardActions: function(){
return this.scopeActions.filter(x => x.cmd.length === 1 && x.cmd[0].action === 'set-autoar-mode') || [];
},
aspectRatioActions: function(){
return this.scopeActions.filter(x => x.cmd.length === 1 && x.cmd[0].action === 'set-ar') || [];
},
cropModePersistenceActions: function() {
return this.scopeActions.filter(x => x.cmd.length === 1 && x.cmd[0].action === 'set-ar-persistence') || [];
},
stretchActions: function(){
return this.scopeActions.filter(x => x.cmd.length === 1 && x.cmd[0].action === 'set-stretch') || [];
},
keyboardActions: function() {
return this.scopeActions.filter(x => x.cmd.length === 1 && x.cmd[0].action === 'set-keyboard') || [];
},
alignmentActions: function() {
return this.scopeActions.filter(x => x.cmd.length === 1 && x.cmd[0].action === 'set-alignment') || [];
},
otherActions: function() {
return this.scopeActions.filter(x => x.cmd.length > 1) || [];
}
}
}

View File

@ -1,14 +0,0 @@
/**
* Creates deep copy of an object
* @param obj
* @returns
*/
export function _cp(obj) {
try {
return JSON.parse(JSON.stringify(obj));
} catch (e) {
// console.error('Failed to parse json. This probably means that the data we received was not an object. Will return data as-is');
// console.error('data in:', obj, 'error:', e);
return obj;
}
}

View File

@ -1,12 +0,0 @@
export function collectionHas(collection, element): boolean {
for (let i = 0, len = collection.length; i < len; i++) {
if (collection[i] == element) {
return true;
}
}
return false;
}
export function equalish(a: number,b: number, tolerance: number): boolean {
return a > b - tolerance && a < b + tolerance;
}

View File

@ -1,103 +0,0 @@
export type ProcessedElementStyles = {
[x: string]: {
css: string,
pxValue: number
}
};
/**
* Note that this function is written _very_ dangerously
* and includes absolutely no error handling
*/
function getPixelValue(value: string, element?: HTMLElement, prop?: string) {
if (value === undefined || value === null) {
return null;
}
if (value.endsWith('px')) {
// console.log('value ends in px:', value)
return parseFloat(value);
}
if (value.endsWith('%')) {
// console.log('value ends in %:', value)
// This allegedly doesn't work for certain types of properties, allegedly.
const parent = element?.parentElement;
if (parent && prop) {
const parentDimensions = parent.getBoundingClientRect();
return (parseFloat(value) * 0.01) * (prop.includes('height') || ['top', 'bottom'].includes(prop) ? parentDimensions.height : parentDimensions.width);
} else {
return null;
}
}
if (value.endsWith("vw")) {
const viewportWidth = window.innerWidth;
return (parseFloat(value) / 100) * viewportWidth;
}
if (value.endsWith("vh")) {
const viewportHeight = window.innerHeight;
return (parseFloat(value) / 100) * viewportHeight;
}
if (value.endsWith("rem")) {
const rootFontSize = parseFloat(getComputedStyle(document.documentElement).fontSize);
return rootFontSize * parseFloat(value);
}
if (value.endsWith("em")) {
const fontSize = parseFloat(getComputedStyle(element).fontSize);
return fontSize * parseFloat(value);
}
};
export default function getElementStyles(element: HTMLElement, props: string[], theoryProps?: string[]): ProcessedElementStyles {
const stylesheets = document.styleSheets;
const computedStyles = getComputedStyle(element);
const stylesOut = {};
for (const stylesheet of stylesheets) {
// console.log('——————————————— processing stylesheet:', stylesheet);
try {
for (const rule of stylesheet?.cssRules) {
if (rule instanceof CSSStyleRule) {
if (element.matches(rule.selectorText)) {
// console.log('element matches rule:', rule);
for (const property in rule.style) {
if (!props.includes(property)) {
continue;
}
const cssValue = rule.style.getPropertyValue(property);
if (theoryProps?.includes(property)) {
stylesOut[property] = {css: cssValue};
}
const actualValue = computedStyles.getPropertyValue(property);
const theory = getPixelValue(cssValue, element, property);
const practice = getPixelValue(actualValue, element, property);
if (theory && practice && (Math.abs(theory - practice) < 1)) {
stylesOut[property] = {
css: cssValue,
pxValue: theory
}
}
}
}
}
}
} catch (e) {
// Cross-origin styles amy cause problems
}
}
return stylesOut;
}

View File

@ -1,49 +0,0 @@
/**
* Reqzests iframe parent hostname
* @returns
*/
export async function getIframeParentHost(): Promise<string> {
return new Promise<any>((resolve) => {
let resolved = false;
let resendInterval;
function handleMessage(event) {
if (event.data.action === 'uw-parent-hostname') {
resolved = true;
clearInterval(resendInterval);
window.removeEventListener('message', handleMessage);
resolve(event.data.hostname);
}
}
window.addEventListener('message', handleMessage);
resendInterval = setInterval(
() => {
if (!resolved) {
window.parent.postMessage(
{ action: 'uw-get-parent-hostname' },
'*'
);
}
},
500
);
});
}
function handleMessage(event) {
if (event.data.action === 'uw-get-parent-hostname') {
event.source.postMessage(
{action: 'uw-parent-hostname', hostname: window.location.hostname},
'*' as any
)
}
}
export async function setupHostnameReporting() {
window.removeEventListener('message', handleMessage); // setupHostnameReporting may run more than once
window.addEventListener('message', handleMessage);
}

439
src/csui/LoggerUi.vue Normal file
View File

@ -0,0 +1,439 @@
<template>
<div v-if="showLoggerUi" class="root-window flex flex-column overflow-hidden"
@keyup.stop
@keydown.stop
@keypress.stop
>
<div class="header">
<div class="header-top flex flex-row">
<div class="flex-grow">
<h1>{{header.header}}</h1>
</div>
<div class="button flex-noshrink button-header"
@click="hidePopup()"
>
<template v-if="logStringified">Finish logging</template>
<template v-else>Hide popup</template>
</div>
<!-- <div class="button flex-noshrink button-header"
@click="stopLogging()"
>
Stop logging
</div> -->
</div>
<div class="header-bottom">
<div>{{header.subheader}}</div>
</div>
</div>
<div class="content flex flex-row flex-grow overflow-hidden">
<!-- LOGGER SETTINGS PANEL -->
<div class="settings-panel flex flex-noshrink flex-column">
<div class="panel-top flex-nogrow">
<h2>Logger configuration</h2>
</div>
<div class="flex flex-row flex-end w100">
<div v-if="!showTextMode" class="button" @click="showTextMode = true">
<Icon icon="clipboard-plus" style="font-size: 2em"></Icon>&nbsp; Paste config ...
</div>
<div v-else class="button" @click="showTextMode = false">
Back
</div>
</div>
<div class="panel-middle scrollable flex-grow log-config-margin">
<template v-if="showTextMode">
<div ref="settingsEditArea"
style="white-space: pre-wrap; border: 1px solid orange; padding: 10px;"
class="monospace h100"
:class="{'jsonbg': !confHasError, 'jsonbg-error': confHasError}"
contenteditable="true"
@input="updateSettings"
>
{{parsedSettings}}
</div>
</template>
<template v-else>
<JsonObject label="logger-settings"
:value="currentSettings"
:ignoreKeys="{'allowLogging': true}"
@change="updateSettingsUi"
></JsonObject>
</template>
</div>
<div class="flex flex-row flex-end">
<div class="button" @click="restoreLoggerSettings()">
Revert
</div>
<div class="button button-primary" @click="saveLoggerSettings()">
Save
</div>
</div>
</div>
<!-- LOGGER OUTPUT/START LOGGING -->
<div class="results-panel flex flex-shrink flex-column overflow-hidden">
<div class="panel-top flex-nogrow">
<h2>Logger results</h2>
</div>
<template v-if="logStringified">
<div v-if="confHasError" class="warn">
Logger configuration contains an error. You can export current log, but you will be unable to record a new log.
</div>
<div class="panel-middle scrollable flex-grow p-t-025em">
<pre>
{{logStringified}}
</pre>
</div>
<div class="flex-noshrink flex flex-row flex-end p-t-025em">
<div class="button button-bar"
@click="startLogging()"
>
New log
</div>
<div class="button button-bar"
@click="exportLog()"
>
Export log
</div>
<div class="button button-bar button-primary"
@click="exportAndQuit()"
>
Export & finish
</div>
</div>
</template>
<template v-else>
<div class="panel-middle scrollable flex-grow">
<div v-if="!parsedSettings" class="text-center w100">
Please paste logger config into the text box to the left.
</div>
<div v-else-if="confHasError" class="warn">
Logger configuration contains an error. Cannot start logging.
</div>
<div v-else-if="lastSettings && lastSettings.allowLogging && lastSettings.consoleOptions && lastSettings.consoleOptions.enabled"
class="flex flex-column flex-center flex-cross-center w100 h100"
>
<p class="m-025em">
Logging in progress ...
</p>
<div class="button button-big button-primary"
@click="stopLogging()"
>
Stop logging
</div>
<template v-if="lastSettings && lastSettings.timeout"
class="m-025em"
>
<p>
... or wait until logging ends.
</p>
<p>
You can <a @click="hidePopup()">hide popup</a> it will automatically re-appear when logging finishes.
</p>
</template>
<template v-else-if="lastSettings">
<p>
You can <a @click="hidePopup()">hide popup</a> the logging will continue until you re-open the popup and stop it.
</p>
</template>
</div>
<div v-else class="flex flex-column flex-center flex-cross-center w100 h100">
<div class="button button-big button-primary"
@click="startLogging()"
>
Start logging
</div>
</div>
</div>
</template>
</div>
</div>
<!-- <div>
button row is heres
</div> -->
</div>
</template>
<script>
import { mapState } from 'vuex';
import Logger from '../ext/lib/Logger';
import Comms from '../ext/lib/comms/Comms';
import IO from '../common/js/IO';
import JsonObject from '../common/components/JsonEditor/JsonObject';
import Icon from '../common/components/Icon';
export default {
components: {
JsonObject,
Icon,
},
data() {
return {
showLoggerUi: false,
header: {
header: 'whoopsie daisy',
subheader: 'you broke the header choosing script'
},
parsedSettings: '',
lastSettings: {},
currentSettings: {},
confHasError: false,
logStringified: '',
showTextMode: false,
}
},
async created() {
const headerRotation = [{
header: "DEFORESTATOR 5000",
subheader: "Iron Legion's finest logging tool"
}, {
header: "Astinus",
subheader: "Ultrawidify logging tool"
}, {
header: "Tracer",
subheader: "I'm already printing stack traces"
}];
this.header = headerRotation[Math.floor(+Date.now() / (3600000*24)) % headerRotation.length] || this.header;
this.getLoggerSettings();
},
computed: {
...mapState([
'uwLog',
'showLogger',
'loggingEnded',
]),
},
watch: {
uwLog(newValue, oldValue) {
if (oldValue !== newValue) {
this.$store.dispatch('uw-show-logger');
this.logStringified = JSON.stringify(newValue, null, 2);
}
},
async showLogger(newValue) {
this.showLoggerUi = newValue;
// update logger settings (they could have changed while the popup was closed)
if (newValue) {
this.getLoggerSettings();
}
},
loggingEnded(newValue) {
// note the value of loggingEnded never actually matters. Even if this value is 'true'
// internally, vuexStore.dspatch() will still do its job and give us the signal we want
if (newValue) {
this.stopLogging();
}
}
},
methods: {
async getLoggerSettings() {
this.lastSettings = await Logger.getConfig() || {};
this.parsedSettings = JSON.stringify(this.lastSettings, null, 2) || '';
this.currentSettings = JSON.parse(JSON.stringify(this.lastSettings));
},
updateSettings(val) {
try {
this.parsedSettings = JSON.stringify(JSON.parse(val.target.textContent.trim()), null, 2);
this.lastSettings = JSON.parse(val.target.textContent.trim());
this.currentSettings = JSON.parse(JSON.stringify(this.lastSettings));
this.confHasError = false;
} catch (e) {
this.confHasError = true;
}
},
updateSettingsUi(val) {
try {
this.parsedSettings = JSON.stringify(val, null, 2);
this.lastSettings = val;
this.currentSettings = JSON.parse(JSON.stringify(this.lastSettings));
} catch (e) {
}
},
saveLoggerSettings() {
Logger.saveConfig({...this.lastSettings});
},
restoreLoggerSettings() {
this.getLoggerSettings();
this.confHasError = false;
},
async startLogging(){
this.logStringified = undefined;
await Logger.saveConfig({...this.lastSettings, allowLogging: true});
window.location.reload();
},
hidePopup() {
// this function only works as 'close' if logging has finished
if (this.logStringified) {
Logger.saveConfig({...this.lastSettings, allowLogging: false});
this.logStringified = undefined;
}
this.$store.dispatch('uw-hide-logger');
},
closePopupAndStopLogging() {
Logger.saveConfig({...this.lastSettings, allowLogging: false});
this.logStringified = undefined;
this.$store.dispatch('uw-hide-logger');
},
stopLogging() {
Logger.saveConfig({...this.lastSettings, allowLogging: false});
this.lastSettings.allowLogging = false;
},
exportLog() {
IO.csStringToFile(this.logStringified);
},
exportAndQuit() {
this.exportLog();
this.logStringified = undefined;
this.closePopupAndStopLogging();
}
}
}
</script>
<style lang="scss" src="../res/css/flex.scss" scoped></style>
<style lang="scss" scoped>
@import '../res/css/colors.scss';
@import '../res/css/font/overpass.css';
@import '../res/css/font/overpass-mono.css';
@import '../res/css/common.scss';
.uw-ultrawidify-container-root {
.root-window {
position: fixed !important;
top: 5vh !important;
left: 5vw !important;
width: 90vw !important;
height: 90vh !important;
z-index: 999999 !important;
background-color: rgba( $page-background, 0.9) !important;
color: #f1f1f1 !important;
font-size: 14px !important;
box-sizing: border-box !important;
}
div {
font-family: 'Overpass';
}
h1, h2 {
font-family: 'Overpass Thin';
}
h1 {
font-size: 4em;
}
h2 {
font-size: 2em;
}
.header {
h1 {
margin-bottom: -0.20em;
margin-top: 0.0em;
}
.header-top, .header-bottom {
padding-left: 16px;
padding-right: 16px;
}
.header-top {
background-color: $popup-header-background !important;
}
.header-bottom {
font-size: 1.75em;
}
}
.content {
box-sizing: border-box;
padding: 8px 32px;
width: 100%;
}
.settings-panel {
box-sizing: border-box;
padding-right: 8px;
flex-grow: 2 !important;
min-width: 30% !important;
flex-shrink: 0 !important;
height: inherit !important;
}
.results-panel {
box-sizing: border-box;
padding-left: 8px;
max-width: 70% !important;
flex-grow: 5 !important;
flex-shrink: 0 !important;
height: inherit !important;
}
.scrollable {
overflow: auto;
}
.overflow-hidden {
overflow: hidden;
}
pre {
font-family: 'Overpass Mono';
}
.m-025em {
margin: 0.25em;
}
.p-t-025em {
padding-top: 0.25em;
}
.button {
display: inline-flex;
align-items: center;
justify-items: center;
padding-left: 2em;
padding-right: 2em;
}
.button-primary {
background-color: $primary;
color: #fff;
}
.button-big {
font-size: 1.5em;
padding: 1.75em 3.25em;
}
.button-bar {
font-size: 1.25em;
padding: 0.25em 1.25em;
margin-left: 0.25em;
}
.button-header {
font-size: 2em;
padding-top: 0.1em;
padding-left: 1em;
padding-right: 1em;
}
.jsonbg {
background-color: #131313;
}
.jsonbg-error {
background-color: #884420;
}
.log-config-margin {
margin-top: 3em;
margin-bottom: 3em;
}
}
</style>

216
src/csui/NotificationUi.vue Normal file
View File

@ -0,0 +1,216 @@
<template>
<div v-if="showNotification" class="uw-ultrawidify-container flex flex-column overflow-hidden">
<div class="notification-popup flex flex-row">
<div v-if="notificationIcon" class="flex-nogrow flex-noshrink notification-icon">
<Icon
class="flex-nogrow flex-noshrink"
:icon="notificationIcon"
>
</Icon>
</div>
<div class="notification-content flex-grow flex-shrink flex flex-column flex-cross-center">
<div
class="notification-text"
v-html="notificationText"
>
</div>
<div
v-if="notificationActions"
class="action-buttons flex flex-row"
>
<div
v-for="action of notificationActions"
class="action-button"
:key="action"
@click="action.command"
>
<Icon v-if="action.icon" :icon="action.icon"></Icon>{{action.label}}
</div>
</div>
<div
v-if="hideActions"
class="hide-actions"
>
Never show again:<wbr>&nbsp;
<template
v-for="action of hideActions"
:key="action"
>
<i @click="closeNotification">
<a
class="hide-action-button"
@click="action.command"
>
{{action.label}}
</a>
<wbr>&nbsp;
</i>
</template>
</div>
</div>
<div
class="notification-icon action-button"
@click="closeNotification()"
>
<Icon
class="flex-nogrow flex-noshrink"
icon="x"
>
</Icon>
</div>
</div>
</div>
</template>
<script>
import { mapState } from 'vuex';
import Icon from '../common/components/Icon';
export default {
components: {
Icon,
},
data() {
return {
notificationTimeout: null,
notificationIcon: "exclamation-triangle",
notificationText: "<b>Sample text.</b> This will be replaced with real notification later.",
notificationActions: null,
hideActions: null,
showNotification: false,
};
},
computed: {
...mapState([
'notificationConfig'
]),
},
watch: {
/**
* Sets new notification config. Currently, we can only show one notification at a time.
*
* We expect a config object like this:
* {
* timeout: number how long we'll be displaying the notification. If empty, 10s. -1: until user dismisses it
* icon: string what icon we're gonna show. We're using bootstrap icons. Can be empty.
* text: notification text. Supports HTML.
* notificationActions: [
* {
* command: function that gets executed upon clicking the button.
* label: label of the button
* icon: icon of the button
* }
* ],
* hideOptions: [
* // more of notificationActions except it's a special case
* ]
* }
*/
notificationConfig(newConfig) {
if (newConfig) {
this.notificationText = newConfig.text;
this.notificationActions = newConfig.notificationActions;
this.notificationIcon = newConfig.icon;
this.hideActions = newConfig.hideActions;
this.showNotification = true;
if (newConfig.timeout !== -1) {
this.notificationTimeout = setTimeout(() => this.closeNotification(), newConfig.timeout ?? 5000);
}
}
}
},
methods: {
closeNotification() {
clearTimeout(this.notificationTimeout);
this.showNotification = false;
this.notificationIcon = null;
this.notificationText = null;
this.notificationActions = null;
this.hideActions = null;
}
}
}
</script>
<style lang="scss" scoped>
@import '../res/css/uwui-base.scss';
@import '../res/css/colors.scss';
@import '../res/css/font/overpass.css';
@import '../res/css/font/overpass-mono.css';
@import '../res/css/common.scss';
.uw-ultrawidify-container-root {
position: relative;
width: 100%;
height: 100%;
pointer-events: none;
display: block !important;
position: relative !important;
width: 100% !important;
height: 100% !important;
pointer-events: none !important;
font-size: 16px !important;
.notification-popup {
pointer-events: auto !important;
position: absolute;
z-index: 99999999;
top: 2em;
right: 2em;
width: 32em;
padding: 0.7em 0.5em;
font-family: 'Overpass';
background-color: rgba(108, 55, 12, 0.779);
color: #fff;
user-select: none;
}
.notifcation-content {
margin-left: 0.5em;
}
.notification-text {
text-align: justify;
padding-left: 0.5em;
padding-right: 0.25em;
}
.notification-icon {
font-size: 3em;
line-height: 0.5;
}
.action-button {
pointer-events: auto;
cursor: pointer;
}
.hide-actions {
color: #ccc;
font-size: 0.8em;
justify-self: flex-end;
align-self: flex-end;
margin-top: 1em;
margin-bottom: -1em;
}
.hide-action-button {
color: #eee;
font-size: 0.9em;
text-decoration: underline;
text-decoration-color: rgba(255,255,255,0.5);
pointer-events: auto;
cursor: pointer;
}
}
</style>

View File

@ -0,0 +1,219 @@
<template>
<div v-if="uiVisible" class="uw-hover uv-hover-trigger-region">
<div class="flex flex-row pad">
<div class="text panel">
<h1>Microsoft Edge's DRM implementation is too broken for this extension to work on this site</h1>
<p><sup>2020-12-22</sup></p>
<p>
In October 2020, Microsoft Edge received an update that renders video playback completely broken on sites that utilize DRM. This extension relies on the browser implementing functional video playback,
which means that this extension currently cannot work in Edge on sites that utilize DRM (and neither can other 21:9 extensions).
</p>
<p>
<b>Using this extension (and its alternatives) on this site may make things worse</b> even if you only set aspect ratio manually.
</p>
<p>
I have attempted all possible workarounds and none of them work.
</p>
<p><small>Yes I do recognize the irony of getting pissed at Microsoft for their bugs while my extension is basically the Skyrim of CWS, but guys at Microsoft are getting paid for working on MS Edge and I'm not getting paid for writing this extension.</small></p>
<p>
<b>I am not keeping up with Edge updates as I primarily use Firefox and Chrome. If Edge has already fixed their broken video implementation,
please open an issue on <a href="https://github.com/tamius-han/ultrawidify/issues" target="_blank">github</a>, <a href="mailto:tamius.han@gmail.com" target="_blank">shoot me an email</a> or
<a href="https://www.reddit.com/message/compose?to=xternal7" target="_blank">PM me on reddit</a>. Please include this string in the message:
</b>
</p>
<p>
<i>{{userAgent}}</i>
</p>
<p>
<b>Your help is much appreciated.</b>
</p>
<p>&nbsp;</p>
<p>
Further reading: <a href="https://stuff.tamius.net/sacred-texts/2020/12/22/ultrawidify-and-edge-2020-edition/" target="_blank">blog post with extra details</a>.
</p>
<p>
I know better than you: <b><a @click="uiVisible=false">hide this popup</a></b>
</p>
<p>
In order to disable this popup forever, open the ultrawidify popup, click on 'site settings' and disable automatic aspect ratio detection for this site.
You should probably even disable the extension for this site altogether for the time being.
</p>
<p>
<br/>
<br/>
<br/>
<br/>
</p>
<h3>Help by reporting this issue to Microsoft</h3>
<p><b>Go to the settings menu</b> <small>(upper right corner of the window, three dots)</small> <b> Help and feedback</b> <small>(second option from the bottom)</small> <b> Send feedback.</b> (Alternatively, press Alt + Shift + I)</p>
<p>Enter this in the first box:</p>
<p>
<br/>
<i>
Videos on sites that utilize DRM protection schemes are not being scaled correctly. If a part of a DRM-protected video is being displayed outside the boundaries of the browser window,
Edge will scale the video to only fit the portion of the video tag that is currently being displayed on the screen, rather than filling the entire video tag. This causes videos appear
differently than website developers intended at best, and breaking certain websites at worst.
</i>
<br/>
</p>
<p>Or something along these lines. Click 'send' when you're done.</p>
<p>Maybe if Edge developer team gets enough reports, they'll fix the problem.</p>
</div>
<div class="image-examples panel">
<p>Follow-up questions</p>
<h3>How can one tell when Edge has fixed their bugs?</h3>
<p>
When 21:9 movies on netflix look like this:
</p>
<p>
<img :src="getUrl('res/img/git-ignore/edge-demo-working.jpg')" />
</p>
<p>
And not like this:
</p>
<p>
<img :src="getUrl('res/img/git-ignore/edge-demo.jpg')" />
</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
</div>
</div>
</div>
</template>
<script>
import { mapState } from 'vuex';
import Icon from '../common/components/Icon';
import BrowserDetect from '../ext/conf/BrowserDetect';
import Settings from '../ext/lib/Settings';
export default {
components: {
Icon,
},
data() {
return {
uiVisible: true,
userAgent: window.navigator.userAgent
};
},
computed: {
},
watch: {
showUi(visible) {
if (visible !== undefined) {
this.uiVisible = visible;
}
}
},
methods: {
getUrl(url) {
return BrowserDetect.firefox ? browser.runtime.getURL(url) : chrome.runtime.getURL(url);
},
async hidePopupForever() {
const settings = new Settings();
await settings.init();
if (!settings.active.mutedNotifications) {
settings.active.mutedNotifications = {};
}
if (!settings.active.mutedNotifications?.browserSpecific) {
settings.active.mutedNotifications.browserSpecific = {
edge: {
brokenDrm: {
}
}
};
}
settings.active.mutedNotifications.browserSpecific.edge.brokenDrm[window.location.hostname] = true;
await settings.saveWithoutReload();
this.uiVisible = false;
}
}
}
</script>
<style lang="scss" src="../res/css/uwui-base.scss"></style>
<style lang="scss" scoped>
@import '../res/css/uwui-base.scss';
@import '../res/css/colors.scss';
@import '../res/css/font/overpass.css';
@import '../res/css/font/overpass-mono.css';
@import '../res/css/common.scss';
.uw-ultrawidify-container-root {
font-family: 'Overpass';
.uw-hover {
position: absolute;
top: 5%;
left: 5%;
width: 90%;
height: 90%;
color: #fff;
background-color: rgba(84, 8, 8, 0.786);
pointer-events: auto;
overflow: hidden;
p, h1, h2, h3 {
margin: 0.75em;
display: block;
// display: inline-block;
}
i {
font-family: 'Overpass Mono', monospace;
}
a {
cursor: pointer;
}
p {
font-size: 16px;
}
h1 {
font-size: 36px;
}
sup {
vertical-align: super;
font-size: smaller;
}
.text {
flex-grow: 1;
flex-shrink: 0;
max-width: 666px;
min-width: 420px;
}
.image-examples {
flex-grow: auto;
flex-shrink: 1;
min-width: 720px;
img {
width: 100%;
}
}
}
.pad {
padding: 16px;
height: 100%;
width: 100%;
// padding-bottom: 10%;
overflow-y: auto;
}
.flex-row {
justify-content: space-around;
}
}
</style>

View File

@ -1,145 +0,0 @@
import Debug from '@src/ext/conf/Debug';
import CommsClient, { CommsOrigin } from '@src/ext/module/comms/CommsClient';
import EventBus from '@src/ext/module/EventBus';
import KeyboardHandler from '@src/ext/module/kbm/KeyboardHandler';
import { ComponentLogger } from '@src/ext/module/logging/ComponentLogger';
import { BLANK_LOGGER_CONFIG, LogAggregator } from '@src/ext/module/logging/LogAggregator';
import Settings from '@src/ext/module/settings/Settings';
import { SiteSettings } from '@src/ext/module/settings/SiteSettings';
import UI from '@src/ext/module/uwui/UI';
import PageInfo from '@src/ext/module/video-data/PageInfo';
import { getIframeParentHost, setupHostnameReporting } from '@src/common/utils/getHost';
export default class UWContent {
pageInfo: PageInfo;
comms: CommsClient;
settings: Settings;
siteSettings: SiteSettings;
keyboardHandler: KeyboardHandler;
logAggregator: LogAggregator;
logger: ComponentLogger;
eventBus: EventBus;
isIframe: boolean = false;
parentHostname: string;
globalUi: any;
commsHandlers: {
[x: string]: ((a: any, b?: any) => void | Promise<void>)[]
} = {
}
constructor(){
setupHostnameReporting();
this.isIframe = window.self !== window.top;
}
reloadSettings() {
try {
this.logger.debug('reloadSettings', 'Things happened in the popup. Will reload extension settings.');
this.init();
} catch (e) {
console.warn('Ultrawidify: settings reload failed. This probably shouldn\'t outright kill the extension, but page reload is recommended.');
}
}
async init(){
if (this.isIframe) {
this.parentHostname = await getIframeParentHost();
console.warn('[uw-content] got iframe parent:', this.parentHostname);
}
try {
if (Debug.debug) {
console.log("[uw::main] loading configuration ...");
}
// logger init is the first thing that needs to run
try {
if (!this.logger) {
this.logAggregator = new LogAggregator('◈');
this.logger = new ComponentLogger(this.logAggregator, 'UWContent');
await this.logAggregator.init(BLANK_LOGGER_CONFIG);
}
} catch (e) {
console.error("logger init failed!", e)
}
// init() is re-run any time settings change
if (this.comms) {
this.comms.destroy();
}
if (this.eventBus) {
this.eventBus.destroy();
}
if (!this.settings) {
this.settings = new Settings({
onSettingsChanged: () => this.reloadSettings(),
logAggregator: this.logAggregator
});
await this.settings.init();
this.siteSettings = this.settings.getSiteSettings({site: window.location.hostname, isIframe: this.isIframe, parentHostname: this.parentHostname});
}
this.eventBus = new EventBus({name: 'content-script', commsOrigin: CommsOrigin.ContentScript});
this.eventBus.subscribe(
'uw-restart',
{
source: this,
function: () => this.initPhase2()
}
);
this.comms = new CommsClient('content-main-port', this.logAggregator, this.eventBus);
this.eventBus.setComms(this.comms);
this.initPhase2();
} catch (e) {
console.error('Ultrawidify initialization failed for some reason:', e);
}
}
// we always initialize extension, even if it's disabled.
initPhase2() {
try {
if (this.pageInfo) {
this.logger.info('setup', 'An instance of pageInfo already exists and will be destroyed.');
this.pageInfo.destroy();
}
this.pageInfo = new PageInfo(this.eventBus, this.siteSettings, this.settings, this.logAggregator);
this.logger.debug('setup', "pageInfo initialized.");
this.logger.debug('setup', "will try to initiate KeyboardHandler.");
if (this.keyboardHandler) {
this.keyboardHandler.destroy();
}
this.keyboardHandler = new KeyboardHandler(this.eventBus, this.siteSettings, this.settings, this.logAggregator);
this.keyboardHandler.init();
this.logger.debug('setup', "KeyboardHandler initiated.");
if (this.globalUi) {
this.globalUi.destroy();
}
// this.globalUi = new UI('ultrawidify-global-ui', {eventBus: this.eventBus, isGlobal: true});
// this.globalUi.enable();
// this.globalUi.setUiVisibility(false);
} catch (e) {
console.error('Ultrawidify: failed to start extension. Error:', e)
this.logger.error('setup', "FAILED TO START EXTENSION. Error:", e);
}
}
destroy() {
this.eventBus.unsubscribeAll(this);
if (this.pageInfo) {
this.pageInfo.destroy();
}
if (this.keyboardHandler) {
this.keyboardHandler.destroy();
}
}
}

View File

@ -1,437 +0,0 @@
import { Runtime } from 'chrome';
import EventBus from '@src/ext/module/EventBus';
import Settings from '@src/ext/module/settings/Settings';
import { ComponentLogger } from '@src/ext/module/logging/ComponentLogger';
import { BLANK_LOGGER_CONFIG, LogAggregator } from '@src/ext/module/logging/LogAggregator';
import CommsServer from '@src/ext/module/comms/CommsServer';
import BrowserDetect from '@src/ext/conf/BrowserDetect';
import { HostInfo } from '@src/common/interfaces/HostData.interface';
import { ExtensionEnvironment } from '@src/common/interfaces/SettingsInterface';
import { CommsOrigin } from '@src/ext/module/comms/CommsClient';
const BASE_LOGGING_STYLES = {
'log': 'background-color: #243; color: #4a8',
}
export default class UWServer {
settings: Settings;
logger: ComponentLogger;
logAggregator: LogAggregator;
comms: CommsServer;
eventBus: EventBus;
ports: any[] = [];
hasVideos: boolean;
currentSite: string = '';
videoTabs: any = {};
currentTabId: number = 0;
selectedSubitem: any = {
'siteSettings': undefined,
'videoSettings': undefined,
}
eventBusCommands = {
'popup-set-selected-tab': {
function: (message) => this.setSelectedTab(message.selectedMenu, message.selectedSubitem)
},
'has-video': {
function: (message, context) => this.registerVideo(context.comms.sender)
},
'noVideo' : {
function: (message, context) => this.unregisterVideo(context.comms.sender)
},
'inject-css': {
function: (message, context) => this.injectCss(message.cssString, context.comms.sender)
},
'eject-css': {
function: (message, context) => this.removeCss(message.cssString, context.comms.sender)
},
'replace-css': {
function: (message, context) => this.replaceCss(message.oldCssString, message.newCssString, context.comms.sender)
},
'get-current-site': {
function: (message, context) => this.getCurrentSite(context.comms.sender)
}
};
private gcTimeout: any;
uiLoggerInitialized: boolean = false;
//#region getters
get activeTab() {
return chrome.tabs.query({currentWindow: true, active: true});
}
//#endregion
constructor() {
this.setup();
}
async setup() {
try {
// logger is the first thing that goes up
const loggingOptions = BLANK_LOGGER_CONFIG;
this.logAggregator = new LogAggregator('🔶bg-script🔶');
this.logger = new ComponentLogger(this.logAggregator, 'UwServer', {styles: BASE_LOGGING_STYLES});
await this.logAggregator.init(loggingOptions);
this.settings = new Settings({logAggregator: this.logAggregator});
await this.settings.init();
this.eventBus = new EventBus({isUWServer: true, commsOrigin: CommsOrigin.Server});
this.eventBus.subscribeMulti(this.eventBusCommands, this);
this.comms = new CommsServer(this);
this.eventBus.setComms(this.comms);
chrome.tabs.onActivated.addListener((m) => {this.onTabSwitched(m)});
} catch (e) {
console.error(`Ultrawidify [server]: failed to start. Reason:`, e);
}
}
async _promisifyTabsGet(browserObj, tabId){
return new Promise( (resolve, reject) => {
browserObj.tabs.get(tabId, (tab) => resolve(tab));
});
}
//#region CSS management
async injectCss(css, sender) {
if (!sender?.tab?.id) {
// console.warn('invalid injectCss received!');
return;
}
this.logger.info('injectCss', 'Trying to inject CSS into tab', sender.tab.id, ', frameId:', sender.frameId, 'css:\n', css)
if (!css) {
return;
}
try {
await chrome.scripting.insertCSS({
target: {
tabId: sender.tab.id,
frameIds: [
sender.frameId
]
},
css,
origin: "USER"
});
} catch (e) {
this.logger.error('injectCss', 'Error while injecting css:', {error: e, css, sender});
}
}
async removeCss(css, sender) {
if (!sender?.tab) {
// console.warn('invalid removeCss received!');
return;
}
try {
await chrome.scripting.removeCSS({
target: {
tabId: sender.tab.id,
frameIds: [
sender.frameId
]
},
css,
origin: "USER"
});
} catch (e) {
this.logger.error('injectCss', 'Error while removing css:', {error: e, css, sender});
}
}
async replaceCss(oldCss, newCss, sender) {
if (!sender?.tab) {
// console.warn('invalid replaceCss received!');
return;
}
if (oldCss !== newCss) {
this.removeCss(oldCss, sender);
this.injectCss(newCss, sender);
}
}
//#endregion
extractHostname(url){
var hostname;
if (!url) {
return "<no url>";
}
// extract hostname
if (url.indexOf("://") > -1) { //find & remove protocol (http, ftp, etc.) and get hostname
hostname = url.split('/')[2];
}
else {
hostname = url.split('/')[0];
}
hostname = hostname.split(':')[0]; //find & remove port number
hostname = hostname.split('?')[0]; //find & remove "?"
return hostname;
}
async onTabSwitched(activeInfo){
this.hasVideos = false;
try {
this.currentTabId = activeInfo.tabId; // just for readability
let tab;
if (BrowserDetect.firefox) {
tab = await chrome.tabs.get(this.currentTabId);
} else if (BrowserDetect.anyChromium) {
tab = await this._promisifyTabsGet(chrome, this.currentTabId);
}
this.currentSite = this.extractHostname(tab.url);
this.logger.info('onTabSwitched', 'user switched tab. New site:', this.currentSite);
} catch(e) {
this.logger.info('onTabSwitched', 'there was a problem getting current site:', e)
}
this.selectedSubitem = {
'siteSettings': undefined,
'videoSettings': undefined,
}
//TODO: change extension icon based on whether there's any videos on current page
}
registerVideo(sender) {
if (!sender?.tab?.url) {
// console.warn('invalid registerVideo received!');
return;
}
this.logger.info('registerVideo', 'Registering video.\nsender:', sender);
const tabHostname = this.extractHostname(sender.tab.url);
const frameHostname = this.extractHostname(sender.url);
// check for orphaned/outdated values and remove them if necessary
if (this.videoTabs[sender.tab.id]?.host != tabHostname) {
delete this.videoTabs[sender.tab.id]
} else if(this.videoTabs[sender.tab.id]?.frames[sender.frameId]?.host != frameHostname) {
delete this.videoTabs[sender.tab.id].frames[sender.frameId];
}
if (this.videoTabs[sender.tab.id]) {
this.videoTabs[sender.tab.id].frames[sender.frameId] = {
id: sender.frameId,
host: frameHostname,
url: sender.url,
registerTime: Date.now(),
}
} else {
this.videoTabs[sender.tab.id] = {
id: sender.tab.id,
host: tabHostname,
url: sender.tab.url,
frames: {}
};
this.videoTabs[sender.tab.id].frames[sender.frameId] = {
id: sender.frameId,
host: frameHostname,
url: sender.url,
registerTime: Date.now(),
}
}
this.logger.info('registerVideo', 'Video registered. current videoTabs:', this.videoTabs);
}
unregisterVideo(sender) {
if (!sender?.tab) {
// console.warn('invalid unregisterVideo received!');
return;
}
this.logger.info('unregisterVideo', 'Unregistering video.\nsender:', sender);
if (this.videoTabs[sender.tab.id]) {
if ( Object.keys(this.videoTabs[sender.tab.id].frames).length <= 1) {
delete this.videoTabs[sender.tab.id]
} else {
if(this.videoTabs[sender.tab.id].frames[sender.frameId]) {
delete this.videoTabs[sender.tab.id].frames[sender.frameId];
}
}
}
this.logger.info('unregisterVideo', 'Video has been unregistered. Current videoTabs:', this.videoTabs);
}
setSelectedTab(menu, subitem) {
this.logger.info('setSelectedTab', 'saving selected tab for', menu, ':', subitem);
this.selectedSubitem[menu] = subitem;
}
async getCurrentSite(sender: Runtime.MessageSender) {
// if (!sender?.tab) {
// console.warn('invalid unregisterVideo received!');
// return;
// }
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});
console.info('get-current-site : returning data:', {site, tabHostname});
this.eventBus.send(
'set-current-site',
{
site,
tabHostname,
},
{
comms: {
forwardTo: 'popup'
}
}
)
}
async getCurrentTab() {
return (await chrome.tabs.query({active: true, currentWindow: true}))[0];
}
private populateFrameVideoStatus(tabId: number, hostnames: string[]) {
const out: HostInfo[] = [];
for (const host of hostnames) {
let video = {hasVideo: false, minEnvironment: ExtensionEnvironment.Normal, maxEnvironment: ExtensionEnvironment.Fullscreen};
for (const frameKey in this.videoTabs[tabId].frames) {
const frame = this.videoTabs[tabId].frames[frameKey];
if (frame.host === host) {
video.hasVideo = true;
if (frame.environment > video.maxEnvironment) {
video.maxEnvironment = frame.environment;
}
if (frame.environment < video.minEnvironment) {
video.minEnvironment = frame.environment;
}
}
}
out.push({
host,
...video,
})
}
return out;
}
private _lastVideoTabData: any | undefined;
async getVideoTab() {
// friendly reminder: if current tab doesn't have a video,
// there won't be anything in this.videoTabs[this.currentTabId]
const ctab = await this.getCurrentTab();
if (!ctab || !ctab.id) {
return {
host: 'INVALID SITE',
frames: [],
hostnames: [],
}
}
const hostnames = await this.comms.listUniqueFrameHosts();
// this probably means we're inside a problematic page
if (!this.videoTabs[ctab.id]) {
return this._lastVideoTabData ?? {
host: 'INVALID SITE',
frames: [],
hostnames: [],
}
}
// if video is older than PageInfo's video rescan period (+ 4000ms of grace),
// we clean it up from videoTabs[tabId].frames array.
const ageLimit = Date.now() - this.settings.active.pageInfo.timeouts.rescan - 4000;
try {
for (const key in this.videoTabs[ctab.id].frames) {
if (this.videoTabs[ctab.id].frames[key].registerTime < ageLimit) {
delete this.videoTabs[ctab.id].frames[key];
}
}
} catch (e) {
// something went wrong. There's prolly no frames.
// this probably shouldn't ever run.
return {
host: this.extractHostname(ctab.url),
hostnames: [],
frames: [],
selected: this.selectedSubitem
}
}
// Ensure hostnames with videos come before hostnames without videos
// Also ensure hostnames are sorted alphabetically
const populatedHostnames = this.populateFrameVideoStatus(ctab.id, hostnames);
populatedHostnames.sort((a: HostInfo, b: HostInfo) => {
return a.hasVideo === b.hasVideo
? a.host === b.host ? 0 : a.host < b.host ? -1 : 1
: a.hasVideo < b.hasVideo ? 1 : -1;
});
this._lastVideoTabData = {
host: this.extractHostname(ctab.url),
hostnames: populatedHostnames.map(x => x.host), // todo: try eliminating this
populatedHostnames: populatedHostnames,
selected: this.selectedSubitem
};
return this._lastVideoTabData;
}
async getCurrentTabHostname() {
const activeTab = await this.activeTab;
if (!activeTab || activeTab.length < 1) {
return null;
}
const url = activeTab[0].url;
if (!url) {
console.log('no URL for active tab:', activeTab[0].url);
}
var hostname;
if (url.indexOf("://") > -1) { //find & remove protocol (http, ftp, etc.) and get hostname
hostname = url.split('/')[2];
}
else {
hostname = url.split('/')[0];
}
hostname = hostname.split(':')[0]; //find & remove port number
hostname = hostname.split('?')[0]; //find & remove "?"
return hostname;
}
}

View File

@ -1,27 +1,27 @@
import VideoAlignmentType from '../../common/enums/VideoAlignmentType.enum';
import StretchType from '../../common/enums/StretchType.enum';
import ExtensionMode from '../../common/enums/ExtensionMode.enum';
import AspectRatioType from '../../common/enums/AspectRatioType.enum';
import CropModePersistence from '../../common/enums/CropModePersistence.enum';
import VideoAlignment from '../../common/enums/video-alignment.enum';
import Stretch from '../../common/enums/stretch.enum';
import ExtensionMode from '../../common/enums/extension-mode.enum';
import AspectRatio from '../../common/enums/aspect-ratio.enum';
import CropModePersistence from '../../common/enums/crop-mode-persistence.enum';
var ActionList = {
'set-ar': {
name: 'Set aspect ratio',
args: [{
name: 'Automatic',
arg: AspectRatioType.Automatic,
arg: AspectRatio.Automatic,
},{
name: 'Fit width',
arg: AspectRatioType.FitWidth,
arg: AspectRatio.FitWidth,
},{
name: 'Fit height',
arg: AspectRatioType.FitHeight,
arg: AspectRatio.FitHeight,
},{
name: 'Reset',
arg: AspectRatioType.Reset,
arg: AspectRatio.Reset,
},{
name: 'Manually specify ratio',
arg: AspectRatioType.Fixed,
arg: AspectRatio.Fixed,
customArg: true,
customSetter: (value) => {
const [width, height] = value.split(':');
@ -70,33 +70,33 @@ var ActionList = {
name: 'Set stretch',
args: [{
name: 'Normal',
arg: StretchType.NoStretch
arg: Stretch.NoStretch
},{
name: 'Basic',
arg: StretchType.Basic,
arg: Stretch.Basic,
},{
name: 'Hybrid',
arg: StretchType.Hybrid,
arg: Stretch.Hybrid,
},{
name: 'Thin borders',
arg: StretchType.Conditional,
arg: Stretch.Conditional,
},{
name: 'Fixed (source)',
arg: StretchType.FixedSource,
arg: Stretch.FixedSource,
customArg: true,
scopes: {
page: true,
}
},{
name: 'Fixed (displayed)',
arg: StretchType.Fixed,
arg: Stretch.Fixed,
customArg: true,
scopes: {
page: true,
}
},{
name: 'Default',
arg: StretchType.Default,
arg: Stretch.Default,
scopes: {
site: true
}
@ -111,16 +111,16 @@ var ActionList = {
name: 'Set video alignment',
args: [{
name: 'Left',
arg: VideoAlignmentType.Left,
arg: VideoAlignment.Left,
},{
name: 'Center',
arg: VideoAlignmentType.Center,
arg: VideoAlignment.Center,
},{
name: 'Right',
arg: VideoAlignmentType.Right
arg: VideoAlignment.Right
},{
name: 'Default',
arg: VideoAlignmentType.Default,
arg: VideoAlignment.Default,
scopes: {
site: true,
}
@ -259,4 +259,4 @@ var ActionList = {
}
};
export default ActionList;
export default ActionList;

View File

@ -1,44 +1,19 @@
import browser from "vuex-webextensions/dist/browser";
if (process.env.CHANNEL !== 'stable') {
console.info('Loaded BrowserDetect');
}
function detectEdgeUA() {
try {
return /Edg\/(\.?[0-9]*)*$/.test(window.navigator.userAgent);
} catch {
return undefined;
}
}
function getBrowserObj() {
return process.env.BROWSER === 'firefox' ? browser : chrome;
}
function getRuntime() {
return process.env.BROWSER === 'firefox' ? browser.runtime : chrome.runtime;
}
function getURL(url) {
return process.env.BROWSER === 'firefox' ? browser.runtime.getURL(url) : chrome.runtime.getURL(url);
}
const BrowserDetect = {
firefox: process.env.BROWSER === 'firefox',
anyChromium: process.env.BROWSER !== 'firefox',
chrome: process.env.BROWSER === 'chrome',
edge: process.env.BROWSER === 'edge',
processEnvBrowser: process.env.BROWSER,
processEnvChannel: process.env.CHANNEL,
isEdgeUA: detectEdgeUA(),
browserObj: getBrowserObj(),
runtime: getRuntime(),
getURL: (url) => getURL(url),
}
isEdgeUA: () => /Edg\/(\.?[0-9]*)*$/.test(window.navigator.userAgent)
}
if (process.env.CHANNEL !== 'stable') {
console.info("BrowserDetect loaded:\n\nprocess.env.BROWSER:", process.env.BROWSER, "\nExporting BrowserDetect:", BrowserDetect);
}
export default BrowserDetect;
export default BrowserDetect;

View File

@ -0,0 +1,425 @@
// How to use:
// version: {ExtensionConf object, but only properties that get overwritten}
import Stretch from '../../common/enums/stretch.enum';
import ExtensionMode from '../../common/enums/extension-mode.enum';
import VideoAlignment from '../../common/enums/video-alignment.enum';
const ExtensionConfPatch = [
{
forVersion: '4.2.0',
sites: {
"old.reddit.com" : {
type: 'testing',
DOM: {
player: {
manual: true,
useRelativeAncestor: false,
querySelectors: '.reddit-video-player-root, .media-preview-content'
}
},
css: '',
},
"www.reddit.com" : {
type: 'testing',
DOM: {
player: {
manual: true,
useRelativeAncestor: false,
querySelectors: '.reddit-video-player-root, .media-preview-content'
}
},
css: '',
},
"www.youtube.com" : {
DOM: {
player: {
manual: true,
querySelectors: "#movie_player, #player",
additionalCss: "",
useRelativeAncestor: false,
playerNodeCss: "",
}
}
},
}
}, {
forVersion: '4.2.3.1',
sites: {
"old.reddit.com" : {
type: 'testing',
DOM: {
player: {
manual: true,
useRelativeAncestor: false,
querySelectors: '.media-preview-content, .reddit-video-player-root'
}
},
css: '',
},
"www.reddit.com" : {
type: 'testing',
DOM: {
player: {
manual: true,
useRelativeAncestor: false,
querySelectors: '.media-preview-content, .reddit-video-player-root'
}
},
css: '',
},
"www.youtube.com" : {
DOM: {
player: {
manual: true,
querySelectors: "#movie_player, #player",
additionalCss: "",
useRelativeAncestor: false,
playerNodeCss: "",
}
}
},
}
}, {
forVersion: '4.3.0',
sites: {
"old.reddit.com" : {
type: 'testing',
DOM: {
player: {
manual: false,
useRelativeAncestor: false,
querySelectors: '.reddit-video-player-root, .media-preview-content'
}
},
css: 'video {\n width: 100% !important;\n height: 100% !important;\n}',
},
"www.reddit.com" : {
type: 'testing',
DOM: {
player: {
manual: false,
useRelativeAncestor: false,
querySelectors: '.reddit-video-player-root, .media-preview-content'
}
},
css: 'video {\n width: 100% !important;\n height: 100% !important;\n}',
},
}
}, {
forVersion: '4.3.1.1',
sites: {
'www.twitch.tv': {
DOM: {
player: {
manual: false,
querySelectors: "",
additionalCss: "",
useRelativeAncestor: false,
playerNodeCss: ""
}
}
}
}
}, {
forVersion: '4.4.0',
updateFn: (userOptions, defaultOptions) => {
// remove 'press P to toggle panning mode' thing
const togglePan = userOptions.actions.find(x => x.cmd && x.cmd.length === 1 && x.cmd[0].action === 'toggle-pan');
if (togglePan) {
togglePan.scopes = {};
}
// add new actions
userOptions.actions.push({
name: 'Don\'t persist crop',
label: 'Never persist',
cmd: [{
action: 'set-ar-persistence',
arg: 0,
}],
scopes: {
site: {
show: true,
},
global: {
show: true,
}
},
playerUi: {
show: true,
}
}, {
userAdded: true,
name: 'Persist crop while on page',
label: 'Until page load',
cmd: [{
action: 'set-ar-persistence',
arg: 1,
}],
scopes: {
site: {
show: true,
},
global: {
show: true,
}
},
playerUi: {
show: true,
}
}, {
userAdded: true,
name: 'Persist crop for current session',
label: 'Current session',
cmd: [{
action: 'set-ar-persistence',
arg: 2,
}],
scopes: {
site: {
show: true,
},
global: {
show: true,
}
},
playerUi: {
show: true,
}
}, {
name: 'Persist until manually reset',
label: 'Always persist',
cmd: [{
action: 'set-ar-persistence',
arg: 3,
}],
scopes: {
site: {
show: true,
},
global: {
show: true,
}
},
playerUi: {
show: true,
}
}, {
name: 'Default crop persistence',
label: 'Default',
cmd: [{
action: 'set-ar-persistence',
arg: -1,
}],
scopes: {
site: {
show: true,
},
},
playerUi: {
show: true,
}
});
// patch shortcuts for non-latin layouts, but only if the user hasn't changed default keys
for (const action of userOptions.actions) {
if (!action.cmd || action.cmd.length !== 1) {
continue;
}
try {
// if this fails, then action doesn't have keyboard shortcut associated with it, so we skip it
const actionDefaults = defaultOptions.actions.find(x => x.cmd && x.cmd.length === 1 // (redundant, default actions have exactly 1 cmd in array)
&& x.cmd[0].action === action.cmd[0].action
&& x.scopes.page
&& x.scopes.page.shortcut
&& x.scopes.page.shortcut.length === 1
&& x.scopes.page.shortcut[0].key === action.scopes.page.shortcut[0].key // this can throw exception, and it's okay
);
if (actionDefaults === undefined) {
continue;
}
// update 'code' property for shortcut
action.scopes.page.shortcut[0]['code'] = actionDefaults.scopes.page.shortcut[0].code;
} catch (e) {
continue;
}
}
}
}, {
forVersion: '4.4.1.1',
sites: {
"www.disneyplus.com": {
DOM: {
player: {
periodicallyRefreshPlayerElement: true,
}
}
},
}
}, {
forVersion: '4.4.2',
updateFn: (userOptions, defaultOptions) => {
try {
userOptions.actions.push(
{
name: 'Stretch source to 4:3',
label: '4:3 stretch (src)',
cmd: [{
action: 'set-stretch',
arg: Stretch.FixedSource,
customArg: 1.33,
}],
scopes: {
page: {
show: true
}
},
playerUi: {
show: true,
path: 'crop'
}
}, {
name: 'Stretch source to 16:9',
label: '16:9 stretch (src)',
cmd: [{
action: 'set-stretch',
arg: Stretch.FixedSource,
customArg: 1.77,
}],
scopes: {
page: {
show: true,
}
},
playerUi: {
show: true,
path: 'crop'
}
});
} catch (e) {
console.error("PROBLEM APPLYING SETTINGS", e);
}
}
}, {
forVersion: '4.4.3.1',
sites: {
"www.disneyplus.com": {
mode: ExtensionMode.Enabled,
autoar: ExtensionMode.Enabled,
autoarFallback: ExtensionMode.Enabled,
override: true, // ignore value localStorage in favour of this
stretch: Stretch.Default,
videoAlignment: VideoAlignment.Default,
keyboardShortcutsEnabled: ExtensionMode.Default,
DOM: {
player: {
periodicallyRefreshPlayerElement: true,
}
}
}
}
}, {
forVersion: '4.4.7',
updateFn: (userOptions, defaultOptions) => {
if (!userOptions.sites['www.netflix.com'].DOM) {
userOptions.sites['www.netflix.com']['DOM'] = {
"player": {
"manual": true,
"querySelectors": ".VideoContainer",
"additionalCss": "",
"useRelativeAncestor": false,
"playerNodeCss": ""
}
}
}
if (!userOptions.sites['www.disneyplus.com']) {
userOptions.sites['www.disneyplus.com'] = {
mode: ExtensionMode.Enabled,
autoar: ExtensionMode.Enabled,
override: false,
type: 'community',
stretch: Stretch.Default,
videoAlignment: VideoAlignment.Default,
keyboardShortcutsEnabled: ExtensionMode.Default,
arPersistence: true, // persist aspect ratio between different videos
autoarPreventConditions: { // prevents autoar on following conditions
videoStyleString: { // if video style string thing does anything of what follows
containsProperty: { // if video style string has any of these properties (listed as keys)
'height': { // if 'height' property is present in style attribute, we prevent autoar from running
allowedValues: [ // unless attribute is equal to anything in here. Optional.
'100%'
]
}
// 'width': true // this would prevent aard from running if <video> had a 'width' property in style, regardless of value
// could also be an empty object, in theory.
}
}
},
DOM: {
"player": {
"manual": true,
"querySelectors": ".btn-media-clients",
"additionalCss": "",
"useRelativeAncestor": false,
"playerNodeCss": ""
}
}
}
} else {
userOptions.sites['wwww.disneyplus.com']['DOM'] = {
"player": {
"manual": true,
"querySelectors": ".btn-media-clients",
"additionalCss": "",
"useRelativeAncestor": false,
"playerNodeCss": ""
}
}
}
}
}, {
forVersion: '4.4.9',
sites: {
"www.youtube.com": {
override: true,
DOM: {
player: {
manual: true
}
}
}
}
}, {
forVersion: '4.5.0',
sites: {
"www.wakanim.tv": {
type: 'community',
DOM: {
player: {
manual: true,
querySelectors: "#jwplayer-container",
additionalCss: "",
useRelativeAncestor: false,
playerNodeCss: "",
}
}
}
}
}, {
forVersion: '4.5.1',
updateFn: (userOptions, defaultOptions) => {
for (const site in userOptions.sites) {
try {
delete userOptions[sites].autoarPreventConditions
} catch (e) {
// doesn't matter if site doesn't have that option,
// everything is still fine
}
}
}
}
];
export default ExtensionConfPatch;

View File

@ -1,539 +0,0 @@
// How to use:
// version: {ExtensionConf object, but only properties that get overwritten}
import AspectRatioType from '@src/common/enums/AspectRatioType.enum';
import CropModePersistence from '@src/common/enums/CropModePersistence.enum';
import EmbeddedContentSettingsOverridePolicy from '@src/common/enums/EmbeddedContentSettingsOverridePolicy.enum';
import ExtensionMode from '@src/common/enums/ExtensionMode.enum';
import { InputHandlingMode } from '@src/common/enums/InputHandlingMode.enum';
import LegacyExtensionMode from '@src/common/enums/LegacyExtensionMode.enum';
import { PlayerDetectionMode } from '@src/common/enums/PlayerDetectionMode.enum';
import { SiteSupportLevel } from '@src/common/enums/SiteSupportLevel.enum';
import SettingsInterface from '@src/common/interfaces/SettingsInterface';
import { _cp } from '@src/common/utils/_cp';
import { update } from 'lodash';
const ExtensionConfPatch = Object.freeze([
{
forVersion: '6.2.4',
updateFn: (userOptions: any, defaultOptions, logger?) => {
for (const site in userOptions.sites) {
(userOptions as any).sites[site].enableUI = {
fullscreen: LegacyExtensionMode.Default,
theater: LegacyExtensionMode.Default,
normal: LegacyExtensionMode.Default,
}
}
userOptions.sites['@global'].enableUI = {
fullscreen: userOptions.ui.inPlayer.enabled ? LegacyExtensionMode.Enabled : LegacyExtensionMode.Disabled,
theater: LegacyExtensionMode.Enabled,
normal: (userOptions.ui.inPlayer.enabled && !userOptions.ui.inPlayer.enabledFullscreenOnly) ? LegacyExtensionMode.Enabled : LegacyExtensionMode.Disabled
}
userOptions.sites['@empty'].enableUI = {
fullscreen: LegacyExtensionMode.Default,
theater: LegacyExtensionMode.Default,
normal: LegacyExtensionMode.Default,
}
}
}, {
forVersion: '6.2.6',
updateFn: (userOptions: any, defaultOptions, logger?) => {
console.log('[ultrawidify] Migrating settings — applying patches for version 6.2.6');
if (!userOptions.commands) {
userOptions.commands = {
zoom: [],
crop: [],
stretch: [],
pan: [],
internal: []
};
}
userOptions.commands.zoom = [{
action: 'change-zoom',
label: 'Zoom +5%',
arguments: {
zoom: 0.05
},
shortcut: {
key: 'z',
code: 'KeyY',
ctrlKey: false,
metaKey: false,
altKey: false,
shiftKey: false,
onKeyUp: true,
onKeyDown: false,
},
internalOnly: true,
actionId: 'change-zoom-10in'
}, {
action: 'change-zoom',
label: 'Zoom -5%',
arguments: {
zoom: -0.05
},
shortcut: {
key: 'u',
code: 'KeyU',
ctrlKey: false,
metaKey: false,
altKey: false,
shiftKey: false,
onKeyUp: true,
onKeyDown: false,
},
internalOnly: true,
actionId: 'change-zoom-10out'
}, {
action: 'set-zoom',
label: 'Reset zoom',
arguments: {
zoom: 1,
},
internalOnly: true,
actionId: 'set-zoom-reset'
}];
delete (userOptions as any).actions;
userOptions.dev = {
loadFromSnapshot: false
};
userOptions.ui.dev = {
aardDebugOverlay: {
showOnStartup: false,
showDetectionDetails: true
}
}
const newZoomActions = [{
action: 'set-zoom',
label: 'Reset zoom',
shortcut: {
key: 'r',
code: 'KeyR',
ctrlKey: false,
metaKey: false,
altKey: false,
shiftKey: true,
onKeyUp: true,
onKeyDown: false,
},
arguments: {
zoom: 1
},
internalOnly: true,
actionId: 'set-zoom-reset'
}, {
action: 'set-ar-zoom',
label: 'Automatic',
comment: 'Automatically detect aspect ratio and zoom accordingly',
arguments: {
type: AspectRatioType.Automatic
},
shortcut: {
key: 'a',
code: 'KeyA',
ctrlKey: false,
metaKey: false,
altKey: false,
shiftKey: true,
onKeyUp: true,
onKeyDown: false,
}
}, {
action: 'set-ar-zoom',
label: 'Cycle',
comment: 'Cycle through zoom options',
arguments: {
type: AspectRatioType.Cycle
},
shortcut: {
key: 'c',
code: 'KeyC',
ctrlKey: false,
metaKey: false,
altKey: false,
shiftKey: true,
onKeyUp: true,
onKeyDown: false,
}
}, {
action: 'set-ar-zoom',
label: '21:9',
comment: 'Zoom for 21:9 aspect ratio (1:2.39)',
arguments: {
type: AspectRatioType.Fixed,
ratio: 2.39
},
shortcut: {
key: 'd',
code: 'KeyD',
ctrlKey: false,
metaKey: false,
altKey: false,
shiftKey: true,
onKeyUp: false,
onKeyDown: true,
}
}, {
action: 'set-ar-zoom',
label: '18:9',
comment: 'Zoom for 18:9 aspect ratio (1:2)',
arguments: {
type: AspectRatioType.Fixed,
ratio: 1.78
},
shortcut: {
key: 's',
code: 'KeyS',
ctrlKey: false,
metaKey: false,
altKey: false,
shiftKey: true,
onKeyUp: false,
onKeyDown: true,
}
}, {
action: 'set-ar-zoom',
label: '32:9',
comment: 'Zoom for 32:9 aspect ratio',
arguments: {
type: AspectRatioType.Fixed,
ratio: 3.56
},
}];
const compareShortcuts = (a: any, b: any) => {
if (!a || !b) {
return false;
}
return a.key === b.key && b.code === b.code && a.ctrlKey == b.ctrlKey && a.shiftKey == b.shiftKey && a.metaKey == a.metaKey && a.altKey == b.altKey;
}
const hasConflict = (shortcut: any) => {
for (const ct in userOptions.commands) {
for (const command of userOptions.commands[ct]) {
if (compareShortcuts(shortcut, command.shortcut)) {
return true;
}
}
}
return false;
}
for (const zoomAction of newZoomActions) {
if (
!userOptions.commands.zoom.find(
x => x.action === zoomAction.action
&& x.arguments?.type === zoomAction.arguments?.type
&& x.arguments?.ratio === zoomAction.arguments?.ratio
)
) {
userOptions.commands.zoom.push({
...zoomAction,
shortcut: hasConflict(zoomAction.shortcut) ? undefined : zoomAction.shortcut
});
}
}
}
}, {
forVersion: '6.3.92',
updateFn: (userOptions: any) => {
// applyToEmbeddedContent is now an enum, and also no longer optional
for (const site in userOptions.sites) {
if (userOptions.sites[site].applyToEmbeddedContent === undefined) {
userOptions.sites[site].applyToEmbeddedContent = EmbeddedContentSettingsOverridePolicy.UseAsDefault;
} else {
userOptions.sites[site].applyToEmbeddedContent = userOptions.sites[site].applyToEmbeddedContent ? EmbeddedContentSettingsOverridePolicy.Always : EmbeddedContentSettingsOverridePolicy.Never;
}
}
}
},
{
forVersion: '6.3.93',
updateFn: (userOptions: SettingsInterface, defaultOptions: SettingsInterface) => {
(userOptions as any).arDetect.polling = defaultOptions.aard.polling;
(userOptions as any).arDetect.subtitles = defaultOptions.aard.subtitles;
(userOptions as any).arDetect.autoDisable = defaultOptions.aard.autoDisable;
}
},
{
forVersion: '6.3.98',
updateFn: (userOptions: SettingsInterface, defaultOptions: SettingsInterface) => {
(userOptions as any).arDetect.letterboxOrientationScan = defaultOptions.aard.letterboxOrientationScan;
(userOptions as any).arDetect.edgeDetection = defaultOptions.aard.edgeDetection;
(userOptions as any).arDetect.subtitles = defaultOptions.aard.subtitles;
}
},
{
forVersion: '6.3.98',
updateFn: (userOptions: SettingsInterface, defaultOptions: SettingsInterface) => {
userOptions.aard = defaultOptions.aard;
userOptions.aardLegacy = defaultOptions.aardLegacy;
delete (userOptions as any).arDetect;
}
},
{
forVersion: '6.3.994',
updateFn: (userOptions: SettingsInterface, defaultOptions: SettingsInterface, logger?) => {
const convertLegacyExtensionMode = (option: {normal: LegacyExtensionMode, theater: LegacyExtensionMode, fullscreen: LegacyExtensionMode}) => {
if (typeof option === 'number') {
logger.warn('updateFn', 'This option is not an object, which suggests it has already been converted. Skipping conversion. Raw value of option:', option);
return option;
}
logger.log(
'updateFn',
`\nconverting option ——— normal: ${LegacyExtensionMode[option.normal]} theater: ${LegacyExtensionMode[option.normal]} fs: ${LegacyExtensionMode[option.fullscreen]}`, '\nraw obj:', option
);
if (option.normal === LegacyExtensionMode.Enabled) {
return ExtensionMode.All;
}
if (option.theater === LegacyExtensionMode.Enabled) {
return ExtensionMode.Theater;
}
if (option.fullscreen === LegacyExtensionMode.Enabled) {
return ExtensionMode.FullScreen;
}
if (option.fullscreen === LegacyExtensionMode.Disabled) {
return ExtensionMode.Disabled;
}
return ExtensionMode.Default;
}
for (const key in userOptions.sites) {
logger.log('updateFn', '\n\n ... migrating default enable-state for site', key);
userOptions.sites[key].enable = convertLegacyExtensionMode(userOptions.sites[key].enable as any);
userOptions.sites[key].enableAard = convertLegacyExtensionMode(userOptions.sites[key].enableAard as any);
if (key === '@global') {
userOptions.sites['@global'].enableKeyboard = userOptions.kbm.enabled && userOptions.kbm.keyboardEnabled ? InputHandlingMode.Enabled : InputHandlingMode.Disabled;
} else {
userOptions.sites[key].enableKeyboard = InputHandlingMode.Default;
}
userOptions.sites[key].enableUI = convertLegacyExtensionMode(
userOptions.sites[key].enableUI ?? (key === '@global' ? ExtensionMode.FullScreen : ExtensionMode.Default) as any
);
logger.log('updateFn', 'migrated site', key, userOptions.sites[key]);
}
}
},
{
forVersion: '6.3.995',
updateFn: (userOptions: SettingsInterface, defaultOptions: SettingsInterface, logger?) => {
if (!userOptions.ui) {
userOptions.ui = defaultOptions.ui
};
}
}, {
forVersion: '6.3.996',
updateFn: (userOptions: SettingsInterface, defaultOptions: SettingsInterface, logger?) => {
for (const site in userOptions.sites) {
const siteData = userOptions.sites[site];
logger.log('updateFn', 'migrating settings for', site, ' — persistCSA?', siteData.persistCSA, 'typeof persistCSA?', typeof siteData.persistCSA, 'does domconfig exist?', siteData.DOMConfig);
if (typeof siteData.persistCSA !== 'number') {
userOptions.sites[site].persistCSA = CropModePersistence.Default;
} else {
userOptions.sites[site].persistCSA = userOptions.sites[site].persistCSA ?? CropModePersistence.Default;
}
if (userOptions.sites[site].type as any === 'user-added' || userOptions.sites[site].type === 'user-defined') {
userOptions.sites[site].type = SiteSupportLevel.UserDefined;
}
if (userOptions.sites[site].type as any === 'no-support') {
userOptions.sites[site].type = SiteSupportLevel.Unknown;
}
if (userOptions.sites[site].defaultType as any === 'user-added' || userOptions.sites[site].defaultType === 'user-defined') {
userOptions.sites[site].type = SiteSupportLevel.UserDefined;
}
if (userOptions.sites[site].defaultType as any === 'no-support') {
userOptions.sites[site].defaultType = SiteSupportLevel.Unknown;
}
if (siteData.activeDOMConfig?.startsWith('community') || siteData.activeDOMConfig === 'official' || siteData.activeDOMConfig === 'empty' || siteData.activeDOMConfig === 'auto') {
siteData.activeDOMConfig = `@${siteData.activeDOMConfig}`;
}
for (const domConf in siteData.DOMConfig) {
logger.log('updateFn', "Updating domconf", domConf);
const oldConf = userOptions.sites[site].DOMConfig[domConf] as any;
logger.log('updateFn', "——— oldConf:", oldConf);
const newConf: any = {
type: oldConf.type ?? userOptions.sites[site].type,
elements: {}
};
if (newConf.type === 'user-added' || newConf.type === 'user-defined') {
newConf.type = SiteSupportLevel.UserDefined;
}
if (newConf.type === 'no-support') {
newConf.type = SiteSupportLevel.Unknown;
}
if (oldConf.elements?.player) {
newConf.elements['player'] = {
detectionMode: oldConf?.elements?.player?.manual ? (
oldConf?.elements?.player?.querySelectors.trim() ? PlayerDetectionMode.QuerySelectors : PlayerDetectionMode.AncestorIndex
) : PlayerDetectionMode.Auto,
allowAutoFallback: true,
ancestorIndex: oldConf?.elements?.player?.parentIndex,
querySelectors: oldConf?.elements?.player?.querySelectors,
}
} else {
newConf.elements['player'] = {
detectionMode: PlayerDetectionMode.Auto,
allowAutoFallback: true,
}
}
if (oldConf.customCss) {
newConf.customCss = oldConf.customCss;
}
if (oldConf.elements?.video) {
newConf.elements['video'] = {
type: oldConf.type ?? siteData.type,
elements: {
video: {
playerDetectionMode: oldConf?.elements?.video?.manual ? PlayerDetectionMode.QuerySelectors : PlayerDetectionMode.Auto,
allowAutoFallback: true,
querySelectors: oldConf?.elements?.video?.querySelectors,
customCSS: oldConf?.elements?.video?.customCss,
}
}
}
}
// migrate names — official and community options get @ at the start
let domConfName = domConf;
if (domConfName.startsWith('community') || domConfName === 'official' || domConfName === 'empty' || domConfName === 'auto') {
domConfName = `@${domConfName}`;
}
if (domConfName !== domConf) {
logger.warn(`updateFn`, '\n\nnaming for default domConf has changed. Old:', domConf, 'new:', domConfName);
delete userOptions.sites[site].DOMConfig[domConf];
}
userOptions.sites[site].DOMConfig[domConfName] = newConf;
}
}
// set new defaults for global and empty:
userOptions.sites['@global'].DOMConfig = {
'@auto': {
type: SiteSupportLevel.Unknown,
elements: {
player: {
detectionMode: PlayerDetectionMode.Auto,
allowAutoFallback: true,
},
video: {
detectionMode: PlayerDetectionMode.Auto,
allowAutoFallback: true,
}
}
}
}
userOptions.sites['@global'].activeDOMConfig = '@auto';
userOptions.sites['@empty'].DOMConfig = {
'@empty': {
type: SiteSupportLevel.UserDefined,
elements: {
player: {
detectionMode: PlayerDetectionMode.Auto,
allowAutoFallback: true,
},
video: {
detectionMode: PlayerDetectionMode.Auto,
allowAutoFallback: true,
}
},
}
};
userOptions.sites['@empty'].activeDOMConfig = '@empty';
logger.log('updateFn', 'Migration complete. New site settings:', userOptions.sites);
}
}, {
forVersion: '6.3.997',
updateFn: (userOptions: SettingsInterface, defaultOptions: SettingsInterface, logger?) => {
// default zoom key combinations that involved the 'shift' key should have
// shortcut.key in uppercase, but they didn't.
for (const command of userOptions.commands?.zoom) {
if (command.shortcut) {
if (command.shortcut.shiftKey) {
if (command.shortcut.key.toUpperCase() === command.shortcut.code.charAt(3)) {
command.shortcut.key = command.shortcut.key.toUpperCase();
}
}
}
}
}
}, {
forVersion: '6.3.998',
updateFn: (userOptions: SettingsInterface, defaultOptions: SettingsInterface, logger?) => {
if (!userOptions.sites["www.amazon.com"]) {
userOptions.sites["www.amazon.com"] = _cp(defaultOptions.sites["www.amazon.com"] );
}
if (userOptions.commands?.zoom) {
const firstFixed = userOptions.commands?.zoom.findIndex(x => x.arguments.type === AspectRatioType.Fixed);
if (firstFixed === -1) {
userOptions.commands.zoom.push({
action: 'set-ar-zoom',
label: 'Cover',
comment: 'Covers the entire screen, cropping as much as needed',
arguments: {
type: AspectRatioType.Cover
},
shortcut: {
key: 'w',
code: 'KeyW',
ctrlKey: false,
metaKey: false,
altKey: false,
shiftKey: true,
onKeyUp: true,
onKeyDown: false,
}
});
} else {
userOptions.commands.zoom.splice(firstFixed, 0, {
action: 'set-ar-zoom',
label: 'Cover',
comment: 'Covers the entire screen, cropping as much as needed',
arguments: {
type: AspectRatioType.Cover
},
shortcut: {
key: 'w',
code: 'KeyW',
ctrlKey: false,
metaKey: false,
altKey: false,
shiftKey: true,
onKeyUp: true,
onKeyDown: false,
}
});
}
}
}
}
]);
export default ExtensionConfPatch;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +0,0 @@
export enum RunLevel {
Off = 0,
UIOnly = 1,
CustomCSSActive = 2
}

View File

@ -0,0 +1,338 @@
import Debug from '../conf/Debug';
import PlayerData from './video-data/PlayerData';
import ExtensionMode from '../../common/enums/extension-mode.enum';
if(process.env.CHANNEL !== 'stable'){
console.info("Loading ActionHandler");
}
class ActionHandler {
constructor(pageInfo) {
this.logger = pageInfo.logger;
this.pageInfo = pageInfo;
this.settings = pageInfo.settings;
this.inputs = ['input', 'select', 'button', 'textarea'];
this.keyboardLocalDisabled = false;
}
init() {
this.logger.log('info', 'debug', "[ActionHandler::init] starting init");
this.keyUpActions = [];
this.keyDownActions = [];
this.mouseMoveActions = [];
this.mouseScrollUpActions = [];
this.mouseScrollDownActions = [];
this.mouseEnterActions = [];
this.mouseLeaveActions = [];
var ths = this;
var actions;
try {
if (this.settings.active.sites[window.location.hostname].actions) {
actions = this.settings.active.sites[window.location.hostname].actions;
} else {
actions = this.settings.active.actions;
}
} catch (e) {
actions = this.settings.active.actions;
}
for (var action of actions) {
if (!action.scopes) {
continue;
}
for (var scope in action.scopes) {
if (! action.scopes[scope].shortcut) {
continue;
}
var shortcut = action.scopes[scope].shortcut[0];
if (shortcut.onKeyDown) {
this.keyDownActions.push({
shortcut: shortcut,
cmd: action.cmd,
scope: scope,
});
}
if (shortcut.onKeyUp) {
this.keyUpActions.push({
shortcut: shortcut,
cmd: action.cmd,
scope: scope,
});
}
if (shortcut.onScrollUp) {
this.mouseScrollUpActions.push({
shortcut: shortcut,
cmd: action.cmd,
scope: scope,
});
}
if (shortcut.onScrollDown) {
this.mouseScrollDownActions.push({
shortcut: shortcut,
cmd: action.cmd,
scope: scope,
});
}
if (shortcut.onMouseEnter) {
this.mouseEnterActions.push({
shortcut: shortcut,
cmd: action.cmd,
scope: scope,
});
}
if (shortcut.onMouseLeave) {
this.mouseLeaveActions.push({
shortcut: shortcut,
cmd: action.cmd,
scope: scope,
});
}
if (shortcut.onMouseMove) {
this.mouseMoveActions.push({
shortcut: shortcut,
cmd: action.cmd,
scope: scope,
});
}
}
}
// events should be handled in handleEvent function. We need to do things this
// way, otherwise we can't remove event listenerđ
// https://stackoverflow.com/a/19507086
document.addEventListener('keydown', this );
document.addEventListener('keyup', this );
this.pageInfo.setActionHandler(this);
this.logger.log('info', 'debug', "[ActionHandler::init] initialization complete");
}
handleEvent(event) {
switch(event.type) {
case 'keydown':
this.handleKeydown(event);
break;
case 'keyup':
this.handleKeyup(event);
break;
case 'mousemove':
this.handleMouseMove(event);
break;
}
}
destroy() {
document.removeEventListener('keydown', this);
document.removeEventListener('keyup', this);
}
registerHandleMouse(videoData) {
this.logger.log('info', ['actionHandler', 'mousemove'], "[ActionHandler::registerHandleMouse] registering handle mouse for videodata:", videoData.id)
var ths = this;
if (videoData.player && videoData.player.element) {
videoData.player.element.addEventListener('mousemove', (event) => ths.handleMouseMove(event, videoData));
}
}
unregisterHandleMouse(videoData) {
var ths = this;
if (videoData.player && videoData.player.element) {
videoData.player.element.removeEventListener('mousemove', (event) => ths.handleMouseMove(event, videoData));
}
}
setKeyboardLocal(state) {
if (state === ExtensionMode.Enabled) {
this.keyboardLocalDisabled = false;
} else if (state === ExtensionMode.Disabled) {
this.keyboardLocalDisabled = true;
}
// don't do shit on invalid value of state
}
preventAction(event) {
var activeElement = document.activeElement;
if (this.logger.canLog('keyboard')) {
this.logger.pause(); // temp disable to avoid recursing;
const preventAction = this.preventAction(event);
this.logger.resume(); // undisable
this.logger.log('info', 'keyboard', "[ActionHandler::preventAction] Testing whether we're in a textbox or something. Detailed rundown of conditions:\n" +
"is full screen? (yes->allow):", PlayerData.isFullScreen(),
"\nis tag one of defined inputs? (yes->prevent):", this.inputs.indexOf(activeElement.tagName.toLocaleLowerCase()) !== -1,
"\nis role = textbox? (yes -> prevent):", activeElement.getAttribute("role") === "textbox",
"\nis type === 'text'? (yes -> prevent):", activeElement.getAttribute("type") === "text",
"\nevent.target.isContentEditable? (yes -> prevent):", event.target.isContentEditable,
"\nis keyboard local disabled? (yes -> prevent):", this.keyboardLocalDisabled,
"\nis keyboard enabled in settings? (no -> prevent)", this.settings.keyboardShortcutsEnabled(window.location.hostname),
"\nwill the action be prevented? (yes -> prevent)", preventAction,
"\n-----------------{ extra debug info }-------------------",
"\ntag name? (lowercase):", activeElement.tagName, activeElement.tagName.toLocaleLowerCase(),
"\nrole:", activeElement.getAttribute('role'),
"\ntype:", activeElement.getAttribute('type'),
"\ninsta-fail inputs:", this.inputs,
"\nevent:", event,
"\nevent.target:", event.target
);
}
// lately youtube has allowed you to read and write comments while watching video in
// fullscreen mode. We can no longer do this.
// if (PlayerData.isFullScreen()) {
// return false;
// }
if (this.keyboardLocalDisabled) {
return true;
}
if (!this.settings.keyboardShortcutsEnabled(window.location.hostname)) {
return true;
}
if (this.inputs.indexOf(activeElement.tagName.toLocaleLowerCase()) !== -1) {
return true;
}
if (activeElement.getAttribute("role") === "textbox") {
return true;
}
if (event.target.isContentEditable) {
return true;
}
if (activeElement.getAttribute("type") === "text") {
return true;
}
return false;
}
isLatin(key) {
return 'abcdefghijklmnopqrstuvwxyz,.-+1234567890'.indexOf(key.toLocaleLowerCase()) !== -1;
}
isActionMatchStandard(shortcut, event) {
return shortcut.key === event.key &&
shortcut.ctrlKey === event.ctrlKey &&
shortcut.metaKey === event.metaKey &&
shortcut.altKey === event.altKey &&
shortcut.shiftKey === event.shiftKey
}
isActionMatchKeyCode(shortcut, event) {
return shortcut.code === event.code &&
shortcut.ctrlKey === event.ctrlKey &&
shortcut.metaKey === event.metaKey &&
shortcut.altKey === event.altKey &&
shortcut.shiftKey === event.shiftKey
}
isActionMatch(shortcut, event, isLatin = true) {
// ASCII and symbols fall back to key code matching, because we don't know for sure that
// regular matching by key is going to work
return isLatin ?
this.isActionMatchStandard(shortcut, event) :
this.isActionMatchStandard(shortcut, event) || this.isActionMatchKeyCode(shortcut, event);
}
execAction(actions, event, videoData) {
this.logger.log('info', 'keyboard', "%c[ActionHandler::execAction] Trying to find and execute action for event. Actions/event: ", "color: #ff0", actions, event);
const isLatin = event.key ? this.isLatin(event.key) : true;
for (var action of actions) {
if (this.isActionMatch(action.shortcut, event, isLatin)) {
this.logger.log('info', 'keyboard', "%c[ActionHandler::execAction] found an action associated with keypress/event: ", "color: #ff0", action);
for (var cmd of action.cmd) {
if (action.scope === 'page') {
if (cmd.action === "set-ar") {
this.pageInfo.setAr({type: cmd.arg, ratio: cmd.customArg});
} else if (cmd.action === "change-zoom") {
this.pageInfo.zoomStep(cmd.arg);
} else if (cmd.action === "set-zoom") {
this.pageInfo.setZoom(cmd.arg);
} else if (cmd.action === "set-stretch") {
this.pageInfo.setStretchMode(cmd.arg);
} else if (cmd.action === "toggle-pan") {
this.pageInfo.setPanMode(cmd.arg)
} else if (cmd.action === "pan") {
if (videoData) {
videoData.panHandler(event, true);
}
} else if (cmd.action === 'set-keyboard') {
this.setKeyboardLocal(cmd.arg);
}
} else {
let site = action.scope === 'site' ? window.location.hostname : '@global';
if (cmd.action === "set-stretch") {
this.settings.active.sites[site].stretch = cmd.arg;
} else if (cmd.action === "set-alignment") {
this.settings.active.sites[site].videoAlignment = cmd.arg;
} else if (cmd.action === "set-extension-mode") {
this.settings.active.sites[site].status = cmd.arg;
} else if (cmd.action === "set-autoar-mode") {
this.settings.active.sites[site].arStatus = cmd.arg;
} else if (cmd.action === 'set-keyboard') {
this.settings.active.sites[site].keyboardShortcutsEnabled = cmd.arg;
} else if (cmd.action === 'set-ar-persistence') {
this.settings.active.sites[site]['cropModePersistence'] = cmd.arg;
this.pageInfo.setArPersistence(cmd.arg);
this.settings.saveWithoutReload();
}
if (cmd.action !== 'set-ar-persistence') {
this.settings.save();
}
}
}
// če smo našli dejanje za to tipko, potem ne preiskujemo naprej
// if we found an action for this key, we stop searching for a match
return;
}
}
}
handleKeyup(event) {
this.logger.log('info', 'keyboard', "%c[ActionHandler::handleKeyup] we pressed a key: ", "color: #ff0", event.key , " | keyup: ", event.keyup, "event:", event);
if (this.preventAction(event)) {
this.logger.log('info', 'keyboard', "[ActionHandler::handleKeyup] we are in a text box or something. Doing nothing.");
return;
}
this.execAction(this.keyUpActions, event);
}
handleKeydown(event) {
this.logger.log('info', 'keyboard', "%c[ActionHandler::handleKeydown] we pressed a key: ", "color: #ff0", event.key , " | keydown: ", event.keydown, "event:", event)
if (this.preventAction(event)) {
this.logger.log('info', 'keyboard', "[ActionHandler::handleKeydown] we are in a text box or something. Doing nothing.");
return;
}
this.execAction(this.keyDownActions, event);
}
handleMouseMove(event, videoData) {
this.logger.log('info', 'keyboard', "[ActionHandler::handleMouseMove] mouse move is being handled.\nevent:", event, "\nvideo data:", videoData);
videoData.panHandler(event);
this.execAction(this.mouseMoveActions, event, videoData)
}
}
if(process.env.CHANNEL !== 'stable'){
console.info("ActionHandler loaded");
}
export default ActionHandler;

12
src/ext/lib/Interface.js Normal file
View File

@ -0,0 +1,12 @@
class Interface {
constructor(videoData) {
this.conf = videoData;
this.player = videoData.player;
}
injectUi() {
this.detectorDiv = document.createElement('div');
this.uiRoot = document.createElement('div');
this.detectorDiv.appendChild(this.uiRoot);
}
}

View File

@ -1,117 +1,42 @@
import currentBrowser from '../conf/BrowserDetect';
import { decycle } from 'json-cyclic';
import Comms from './comms/Comms';
import BrowserDetect from '../conf/BrowserDetect';
if (process.env.CHANNEL !== 'stable'){
console.info('Loading Logger');
}
export const baseLoggingOptions: LoggerConfig = {
isContentScript: true,
allowLogging: false,
useConfFromStorage: true,
fileOptions: {
enabled: false
},
consoleOptions: {
"enabled": false,
"debug": true,
"init": true,
"settings": true,
"keyboard": true,
"mousemove": false,
"kbmHandler": true,
"comms": true,
"playerDetect": true,
"resizer": true,
"scaler": true,
"stretcher": true,
"videoRescan": false,
"playerRescan": false,
"arDetect": true,
"arDetect_verbose": false
},
allowBlacklistedOrigins: {
'periodicPlayerCheck': false,
'periodicVideoStyleChangeCheck': false,
'handleMouseMove': false
}
};
export interface LoggingOptions {
enabled?: boolean;
debug?: boolean;
init?: boolean;
settings?: boolean;
keyboard?: boolean;
mousemove?: boolean;
kbmHandler?: boolean;
comms?: boolean;
playerDetect?: boolean;
resizer?: boolean;
scaler?: boolean;
stretcher?: boolean;
videoRescan?: boolean;
playerRescan?: boolean;
arDetect?: boolean;
arDetect_verbose?: boolean;
}
export interface LoggerBlacklistedOrigins {
periodicPlayerCheck?: boolean;
periodicVideoStyleChangeCheck?: boolean;
handleMouseMove?: boolean;
}
export interface LoggerConfig {
isContentScript?: boolean;
isBackgroundScript?: boolean;
allowLogging?: boolean;
useConfFromStorage?: boolean;
fileOptions?: LoggingOptions;
consoleOptions?: LoggingOptions;
allowBlacklistedOrigins?: LoggerBlacklistedOrigins;
}
class Logger {
temp_disable: boolean = false;
onLogEndCallbacks: any[] = [];
history: any[] = [];
globalHistory: any = {};
isContentScript: boolean = false;
isBackgroundScript: boolean = true;
vuexStore: any;
uwInstance: any;
conf: any;
startTime: number;
stopTime: number;
constructor(options) {
this.onLogEndCallbacks = [];
this.history = [];
this.globalHistory = {};
this.isContentScript = false;
this.isBackgroundScript = true;
constructor(options?: {vuexStore?: any, uwInstance?: any}) {
this.vuexStore = options?.vuexStore;
this.uwInstance = options?.uwInstance;
chrome.storage.onChanged.addListener((changes, area) => {
this.storageChangeListener(changes, area)
});
}
static saveConfig(conf: LoggerConfig) {
console.warn('LEGACY LOGGER IS STILL BEING CALLED FROM SOMEWHERE!', new Error().stack);
static saveConfig(conf) {
if (process.env.CHANNEL === 'dev') {
console.info('Saving logger conf:', conf)
}
chrome.storage.local.set( {'uwLogger': JSON.stringify(conf)});
if (currentBrowser.firefox || currentBrowser.edge) {
return browser.storage.local.set( {'uwLogger': JSON.stringify(conf)});
} else if (currentBrowser.chrome) {
return chrome.storage.local.set( {'uwLogger': JSON.stringify(conf)});
}
}
static syncConfig(callback: (x) => void) {
chrome.storage.onChanged.addListener( (changes: any, area: string) => {
static syncConfig(callback) {
const br = currentBrowser.firefox ? browser : chrome;
br.storage.onChanged.addListener( (changes, area) => {
if (changes.uwLogger) {
const newLoggerConf = JSON.parse(changes.uwLogger.newValue as any)
const newLoggerConf = JSON.parse(changes.uwLogger.newValue)
if (process.env.CHANNEL === 'dev') {
console.info('Logger settings reloaded. New conf:', newLoggerConf);
console.info('Logger settings reloaded. New conf:', conf);
}
callback(newLoggerConf);
}
@ -121,13 +46,17 @@ class Logger {
static async getConfig() {
let ret;
// if (BrowserDetect.firefox) {
ret = await chrome.storage.local.get('uwLogger');
// } else if (BrowserDetect.anyChromium) {
// ret = await new Promise( (resolve, reject) => {
// chrome.storage.local.get('uwLogger', (res) => resolve(res));
// });
// }
if (currentBrowser.firefox) {
ret = await browser.storage.local.get('uwLogger');
} else if (currentBrowser.chrome) {
ret = await new Promise( (resolve, reject) => {
chrome.storage.local.get('uwLogger', (res) => resolve(res));
});
} else if (currentBrowser.edge) {
ret = await new Promise( (resolve, reject) => {
browser.storage.local.get('uwLogger', (res) => resolve(res));
});
}
if (process.env.CHANNEL === 'dev') {
try {
@ -144,7 +73,7 @@ class Logger {
}
}
async init(conf: LoggerConfig) {
async init(conf) {
// this is the only property that always gets passed via conf
// and doesn't get ignored even if the rest of the conf gets
// loaded from browser storage
@ -167,12 +96,14 @@ class Logger {
if (this.conf.fileOptions === undefined) {
this.conf.fileOptions = {};
}
this.startTime = performance.now();
this.temp_disable = false;
this.stopTime = this.conf.timeout ? performance.now() + (this.conf.timeout * 1000) : undefined;
chrome.storage.onChanged.addListener( (changes: any, area: any) => {
const br = currentBrowser.firefox ? browser : chrome;
br.storage.onChanged.addListener( (changes, area) => {
if (process.env.CHANNEL === 'dev') {
if (!changes.uwLogger) {
// console.info('[Logger::<storage/on change> No new logger settings!');
@ -195,39 +126,12 @@ class Logger {
});
}
storageChangeListener(changes: any, area: any) {
if (!changes.uwLogger) {
return;
}
try {
this.conf = JSON.parse(changes.uwLogger.newValue);
} catch (e) {
console.warn('[uwLogger] Error while trying to parse new conf for logger:', e, '\nWe received the following changes:', changes, 'for area:', area);
}
// This code can only execute if user tried to enable or disable logging
// through the popup. In cases like this, we do not gate the console.log
// behind a check, since we _always_ want to have this feedback in response
// to an action.
console.info(
'[uwLogger] logger config changed! New configuration:',
this.conf, '\nraw changes:', changes, 'area?', area,
'\n————————————————————————————————————————————————————————————————————————\n\n\n\n\n\n\n\n\n\n\n\n-----\nLogging with new settings starts now.'
);
// initiate loger if need be
if (!this.startTime) {
this.init(this.conf);
}
}
setVuexStore(store) {
this.vuexStore = store;
}
clear() {
this.history = [];
this.log = [];
this.startTime = performance.now();
this.stopTime = this.conf.timeout ? performance.now() + (this.conf.timeout * 1000) : undefined;
}
@ -238,13 +142,13 @@ class Logger {
Logger.saveConfig(conf);
}
// async getSaved() {
// return Logger.getSaved();
// }
async getSaved() {
return Logger.getSaved();
}
// allow syncing of start times between bg and page scripts.
// may result in negative times in the log file, but that doesn't
// may result in negative times in the log file, but that doesn't
// really matter
getStartTime() {
return this.startTime;
@ -268,19 +172,9 @@ class Logger {
// return logfileStr;
// }
getFileLogJSONString() {
let site;
// NOTE: no more window object on UWServer side of things!
// (or rather, we could get it, but we don't care enough to get it in this instance)
try {
site = window && window.location;
} catch {
site = 'uw-bg';
}
return {
site,
log: JSON.stringify(this.history),
site: window && window.location,
log: JSON.toString(this.history),
}
}
@ -296,7 +190,7 @@ class Logger {
}
// this should be used mostly in background page instance of logger, btw
//
//
addToGlobalHistory(key, log) {
this.globalHistory[key] = log;
this.log('info', 'debug', 'Added log for', key, 'to global history. Current history:', this.globalHistory);
@ -319,7 +213,7 @@ class Logger {
parseStack() {
const trace = (new Error()).stack;
const stackInfo: any = {};
const stackInfo = {};
// we turn our stack into array and remove the "file::line" part of the trace,
// since that is useless because minification/webpack
stackInfo['stack'] = {trace: trace.split('\n').map(a => a.split('@')[0])};
@ -368,8 +262,8 @@ class Logger {
stackInfo.stack.trace.splice(i);
break;
}
}
}
return stackInfo;
}
@ -421,14 +315,14 @@ class Logger {
if (this.isBlacklistedOrigin(stackInfo)) {
return false;
}
}
// if either of these two is true, we allow logging to happen (forbidden origins were checked above)
return (this.canLogFile(component) || this.canLogConsole(component) || stackInfo.exitLogs);
}
canLogFile(component) {
if (!this.conf?.fileOptions?.enabled || this.temp_disable) {
if (!this.conf.fileOptions.enabled || this.temp_disable) {
return false;
}
if (Array.isArray(component) && component.length ) {
@ -442,7 +336,7 @@ class Logger {
}
}
canLogConsole(component, stackInfo?) {
canLogConsole(component, stackInfo) {
if (!this.conf.consoleOptions?.enabled || this.temp_disable) {
return false;
}
@ -458,7 +352,7 @@ class Logger {
return this.conf.logAll;
}
logToFile(message, stackInfo) {
let ts = performance.now();
if (ts <= this.history[this.history.length - 1]) {
@ -475,16 +369,16 @@ class Logger {
logToConsole(level, message, stackInfo) {
try {
switch (level) {
case 'error':
case 'error':
console.error(...message, {stack: stackInfo});
break;
case 'warn':
console.warn(...message, {stack: stackInfo});
break;
case 'info':
case 'info':
console.info(...message, {stack: stackInfo});
break;
default:
default:
console.log(...message, {stack: stackInfo});
}
} catch (e) {
@ -522,7 +416,7 @@ class Logger {
if (this.isBlacklistedOrigin(stackInfo)) {
return;
}
if (this.conf.fileOptions?.enabled) {
if (this.canLogFile(component) || stackInfo.exitLogs) {
this.logToFile(message, stackInfo);
@ -543,7 +437,7 @@ class Logger {
let ts = performance.now();
let secondMark = ts - 1000;
let halfSecondMark = ts - 500;
let i = this.history.length;
let i = this.history.length();
// correct ts _after_ secondMark and halfSecondMark were determined
if (ts <= this.history[this.history.length - 1]) {
@ -555,7 +449,7 @@ class Logger {
message: "-------------------------------------- CAHEN --------------------------------------"
});
// find the spot for the half second mark. In this case, we don't really particularly care whether timestamps
// find the spot for the half second mark. In this case, we don't really particularly care whether timestamps
// are duped due to cahen warnings
while (this.history[i--].ts > halfSecondMark) {}
this.history.push({
@ -603,7 +497,7 @@ class Logger {
Comms.sendMessage({cmd: 'show-logger', forwardToSameFramePort: true, port: 'content-ui-port'});
let exportObject;
let exportObject;
try {
exportObject = {
pageLogs: decycle(this.history),
@ -641,7 +535,7 @@ class Logger {
console.info('[info] vuex store present. Parsing logs.');
let exportObject;
let exportObject;
try {
exportObject = {
pageLogs: decycle(this.history),

View File

@ -1,10 +1,10 @@
import Debug from '../../ext/conf/Debug';
import Debug from '../conf/Debug';
class ObjectCopy {
static addNew(current, newValues){
// clone target
let out = JSON.parse(JSON.stringify(newValues));
var out = JSON.parse(JSON.stringify(newValues));
if(! current) {
if(Debug.debug) {
@ -14,13 +14,13 @@ class ObjectCopy {
return out;
}
for(let k in out) {
for(var k in out) {
// if current key exist, replace it with existing value. Take no action otherwise.
if(current[k]) {
// Types and constructors of objects must match. If they don't, we always use the new value.
if(typeof out[k] === typeof current[k] && out[k].constructor === current[k].constructor) {
// objects are special, we need to check them recursively.
if(out[k] && typeof out[k] === 'object' && out[k].constructor === Object ) {
if(Debug.debug && Debug.settings) {
@ -29,7 +29,7 @@ class ObjectCopy {
out[k] = this.addNew(current[k], out[k]);
} else {
out[k] = current[k];
out[k] = current[k];
}
}
}
@ -37,7 +37,7 @@ class ObjectCopy {
// add the values that would otherwise be deleted back to our object. (We need that so user-defined
// sites don't get forgotten)
for(let k in current) {
for(var k in current) {
if (! out[k]) {
out[k] = current[k];
}
@ -47,12 +47,12 @@ class ObjectCopy {
}
static overwrite(current, newValues){
for(let k in newValues) {
for(var k in newValues) {
// if current key exist, replace it with existing value. Take no action otherwise.
if (current[k] !== undefined) {
// Types and constructors of objects must match. If they don't, we always use the new value.
if (typeof newValues[k] === typeof current[k] && newValues[k].constructor === current[k].constructor) {
// objects are special, we need to check them recursively.
if(current[k] && typeof current[k] === 'object' && current[k].constructor === Object ) {
if(Debug.debug && Debug.settings) {
@ -61,7 +61,7 @@ class ObjectCopy {
current[k] = this.overwrite(current[k], newValues[k]);
} else {
current[k] = newValues[k];
current[k] = newValues[k];
}
} else {
current[k] = newValues[k];
@ -82,4 +82,4 @@ class ObjectCopy {
}
}
export default ObjectCopy;
export default ObjectCopy;

View File

@ -0,0 +1,189 @@
class PlayerPickerHelper {
constructor (settings, callbacks) {
this.settings = settings;
this.videos = document.selectElementsByTagName('video');
this.selectedParentIndex = this.findPlayerForVideos(settings, this.videos)[0];
this.savedCss = [];
this.markVideos();
this.markIndexForAll(index);
this.markInitialQuerySelectors();
}
/*
*
* Internal functions
*
*/
saveBorder(element) {
if (this.savedCss.findIndex(x => x.element === element) !== -1) {
this.savedCss.push({element: element, border: element.style.border});
}
}
restoreBorders() {
for (const e of this.savedCss) {
e.element.style.border = e.border;
}
}
findPlayerForVideos(settings, videos) {
const playerIndexes = [];
for (const v of videos) {
playerIndexes.push(this.findPlayerForVideo(settings, v));
}
return playerIndexes;
}
findPlayerForVideo(settings, video) {
const host = window.location.hostname;
let element = video.parentNode;
if (this.settings.active.sites[host]
&& this.settings.active.sites[host].DOM
&& this.settings.active.sites[host].DOM.player
&& this.settings.active.sites[host].DOM.player.manual) {
if (this.settings.active.sites[host].DOM.player.useRelativeAncestor
&& this.settings.active.sites[host].DOM.player.videoAncestor) {
return this.settings.active.sites[host].DOM.player.videoAncestor;
} else if (this.settings.active.sites[host].DOM.player.querySelectors) {
const allSelectors = document.querySelectorAll(this.settings.active.sites[host].DOM.player.querySelectors);
let elementIndex = 1;
while (element && !this.collectionHas(allSelectors, element)) {
element = element.parentNode;
elementIndex++;
}
return elementIndex;
}
}
let elementIndex = 0;
var trustCandidateAfterGrows = 2; // if candidate_width or candidate_height increases in either dimensions this many
// times, we say we found our player. (This number ignores weird elements)
// in case our <video> is bigger than player in one dimension but smaller in the other
// if site is coded properly, player can't be wider than that
var candidate_width = Math.max(element.offsetWidth, window.innerWidth);
var candidate_height = Math.max(element.offsetHeight, window.innerHeight);
var playerCandidateNode = element;
// if we haven't found element using fancy methods, we resort to the good old fashioned way
var grows = trustCandidateAfterGrows;
while(element != undefined){
// odstranimo čudne elemente, ti bi pokvarili zadeve
// remove weird elements, those would break our stuff
if ( element.offsetWidth == 0 || element.offsetHeight == 0){
element = element.parentNode;
elementIndex++;
continue;
}
if ( element.offsetHeight <= candidate_height &&
element.offsetWidth <= candidate_width ){
// if we're in fullscreen, we only consider elements that are exactly as big as the monitor.
if( ! isFullScreen ||
(element.offsetWidth == window.innerWidth && element.offsetHeight == window.innerHeight) ){
playerCandidateNode = element;
candidate_width = element.offsetWidth;
candidate_height = element.offsetHeight;
grows = trustCandidateAfterGrows;
this.logger.log('info', 'debug', "Found new candidate for player. Dimensions: w:", candidate_width, "h:",candidate_height, "node:", playerCandidateNode);
}
}
else if(grows --<= 0){
this.logger.log('info', 'playerDetect', "Current element grew in comparrison to the child. We probably found the player. breaking loop, returning current result");
break;
}
element = element.parentNode;
elementIndex++;
}
return elementIndex;
}
markVideos() {
for (const v of this.videos) {
this.markVideo(v);
}
}
markVideo(video) {
this.saveBorder(video);
video.style.border = "1px solid #00f";
}
markIndexForAll(index){
for (const v of this.videos) {
this.markIndex(index, v);
}
}
markIndex(index, video) {
el = video.parentNode;
while (index --> 1) {
el = el.parentNode;
}
this.saveBorder(el);
el.style.border = "1px solid #88f";
}
markInitialQuerySelectors() {
try {
if (this.settings.active.sites[host].DOM.player.querySelectors.trim()) {
this.markQuerySelectorMatches(this.settings.active.sites[host].DOM.player.querySelectors);
}
} catch (e) {
// nothing to see here. something in that if spaghett is undefined, which causes
// everything to fail. Since this means we've got zero query string matches to mark,
// we just ignore the failure
}
}
markQuerySelectorMatches(qsString) {
const allSelectors = document.querySelectorAll(qsString);
for (e of allSelectors) {
this.saveBorder(e);
e.style.border = "1px dashed fd2";
}
}
markQsPlayerDetection(qsString, index, video) {
let element = video.parentNode;
let elementIndex = 1;
const allSelectors = document.querySelectorAll(qsString);
while (element && !this.collectionHas(allSelectors, element)) {
element = element.parentNode;
elementIndex++;
}
this.saveBorder(element)
if (elementIndex > index) {
element.style.border = "2px solid #f00"
} else if (elementIndex === index) {
element.style.border = "2px solid #027a5c"
}
}
/*
*
*
* Function that actually interface with playerpicker and do stuff
*
*
*/
setQuerySelectors(querySelectorString) {
}
}
export default PlayerPickerHelper

597
src/ext/lib/Settings.js Normal file
View File

@ -0,0 +1,597 @@
import Debug from '../conf/Debug';
import currentBrowser from '../conf/BrowserDetect';
import ExtensionConf from '../conf/ExtensionConf';
import ExtensionMode from '../../common/enums/extension-mode.enum';
import ObjectCopy from '../lib/ObjectCopy';
import Stretch from '../../common/enums/stretch.enum';
import VideoAlignment from '../../common/enums/video-alignment.enum';
import ExtensionConfPatch from '../conf/ExtConfPatches';
import CropModePersistence from '../../common/enums/crop-mode-persistence.enum';
import BrowserDetect from '../conf/BrowserDetect';
if(process.env.CHANNEL !== 'stable'){
console.info("Loading Settings");
}
class Settings {
constructor(options) {
// Options: activeSettings, updateCallback, logger
this.logger = options?.logger;
this.onSettingsChanged = options?.onSettingsChanged;
this.afterSettingsSaved = options?.afterSettingsSaved;
this.active = options?.activeSettings ?? undefined;
this.default = ExtensionConf;
this.default['version'] = this.getExtensionVersion();
this.useSync = false;
this.version = undefined;
if (currentBrowser.firefox) {
browser.storage.onChanged.addListener((changes, area) => {this.storageChangeListener(changes, area)});
} else if (currentBrowser.chrome) {
chrome.storage.onChanged.addListener((changes, area) => {this.storageChangeListener(changes, area)});
}
}
storageChangeListener(changes, area) {
if (!changes.uwSettings) {
return;
}
this.logger?.log('info', 'settings', "[Settings::<storage/on change>] Settings have been changed outside of here. Updating active settings. Changes:", changes, "storage area:", area);
// if (changes['uwSettings'] && changes['uwSettings'].newValue) {
// this.logger?.log('info', 'settings',"[Settings::<storage/on change>] new settings object:", JSON.parse(changes.uwSettings.newValue));
// }
const parsedSettings = JSON.parse(changes.uwSettings.newValue);
this.setActive(parsedSettings);
this.logger?.log('info', 'debug', 'Does parsedSettings.preventReload exist?', parsedSettings.preventReload, "Does callback exist?", !!this.onSettingsChanged);
if (!parsedSettings.preventReload && this.onSettingsChanged) {
try {
this.onSettingsChanged();
this.logger?.log('info', 'settings', '[Settings] Update callback finished.')
} catch (e) {
this.logger?.log('error', 'settings', "[Settings] CALLING UPDATE CALLBACK FAILED. Reason:", e)
}
}
if (this.afterSettingsSaved) {
this.afterSettingsSaved();
}
}
static getExtensionVersion() {
if (currentBrowser.firefox) {
return browser.runtime.getManifest().version;
} else if (currentBrowser.chrome) {
return chrome.runtime.getManifest().version;
}
}
getExtensionVersion() {
return Settings.getExtensionVersion();
}
compareExtensionVersions(a, b) {
let aa = a.split('.');
let bb = b.split('.');
if (+aa[0] !== +bb[0]) {
// difference on first digit
return +aa[0] - +bb[0];
} if (+aa[1] !== +bb[1]) {
// first digit same, difference on second digit
return +aa[1] - +bb[1];
} if (+aa[2] !== +bb[2]) {
return +aa[2] - +bb[2];
// first two digits the same, let's check the third digit
} else {
// fourth digit is optional. When not specified, 0 is implied
// btw, ++(aa[3] || 0) - ++(bb[3] || 0) doesn't work
// Since some things are easier if we actually have a value for
// the fourth digit, we turn a possible undefined into a zero
aa[3] = aa[3] === undefined ? 0 : aa[3];
bb[3] = bb[3] === undefined ? 0 : bb[3];
// also, the fourth digit can start with a letter.
// versions that start with a letter are ranked lower than
// versions x.x.x.0
if (isNaN(+aa[3]) ^ isNaN(+bb[3])) {
return isNaN(+aa[3]) ? -1 : 1;
}
// at this point, either both version numbers are a NaN or
// both versions are a number.
if (!isNaN(+aa[3])) {
return +aa[3] - +bb[3];
}
// letters have their own hierarchy:
// dev < a < b < rc
let av = this.getPrereleaseVersionHierarchy(aa[3]);
let bv = this.getPrereleaseVersionHierarchy(bb[3]);
if (av !== bv) {
return av - bv;
} else {
return +(aa[3].replace(/\D/g,'')) - +(bb[3].replace(/\D/g, ''));
}
}
}
getPrereleaseVersionHierarchy(version) {
if (version.startsWith('dev')) {
return 0;
}
if (version.startsWith('a')) {
return 1;
}
if (version.startsWith('b')) {
return 2;
}
return 3;
}
sortConfPatches(patchesIn) {
return patchesIn.sort( (a, b) => this.compareExtensionVersions(a.forVersion, b.forVersion));
}
findFirstNecessaryPatch(version, extconfPatches) {
const sorted = this.sortConfPatches(extconfPatches);
return sorted.findIndex(x => this.compareExtensionVersions(x.forVersion, version) > 0);
}
applySettingsPatches(oldVersion, patches) {
let index = this.findFirstNecessaryPatch(oldVersion, patches);
if (index === -1) {
this.logger?.log('info','settings','[Settings::applySettingsPatches] There are no pending conf patches.');
return;
}
// apply all remaining patches
this.logger?.log('info', 'settings', `[Settings::applySettingsPatches] There are ${patches.length - index} settings patches to apply`);
while (index < patches.length) {
const updateFn = patches[index].updateFn;
delete patches[index].forVersion;
delete patches[index].updateFn;
if (Object.keys(patches[index]).length > 0) {
ObjectCopy.overwrite(this.active, patches[index]);
}
if (updateFn) {
try {
updateFn(this.active, this.getDefaultSettings());
} catch (e) {
this.logger?.log('error', 'settings', '[Settings::applySettingsPatches] Failed to execute update function. Keeping settings object as-is. Error:', e);
}
}
index++;
}
}
async init() {
const settings = await this.get();
this.version = this.getExtensionVersion();
// |—> on first setup, settings is undefined & settings.version is haram
// | since new installs ship with updates by default, no patching is
// | needed. In this case, we assume we're on the current version
const oldVersion = (settings && settings.version) || this.version;
if (settings) {
this.logger?.log('info', 'settings', "[Settings::init] Configuration fetched from storage:", settings,
"\nlast saved with:", settings.version,
"\ncurrent version:", this.version
);
}
// if (Debug.flushStoredSettings) {
// this.logger?.log('info', 'settings', "%c[Settings::init] Debug.flushStoredSettings is true. Using default settings", "background: #d00; color: #ffd");
// Debug.flushStoredSettings = false; // don't do it again this session
// this.active = this.getDefaultSettings();
// this.active.version = this.version;
// this.set(this.active);
// return this.active;
// }
// if there's no settings saved, return default settings.
if(! settings || (Object.keys(settings).length === 0 && settings.constructor === Object)) {
this.logger?.log(
'info',
'settings',
'[Settings::init] settings don\'t exist. Using defaults.\n#keys:',
settings ? Object.keys(settings).length : 0,
'\nsettings:',
settings
);
this.active = this.getDefaultSettings();
this.active.version = this.version;
await this.save();
return this.active;
}
// if there's settings, set saved object as active settings
this.active = settings;
// if last saved settings was for version prior to 4.x, we reset settings to default
// it's not like people will notice cos that version didn't preserve settings at all
if (this.active.version && !settings.version.startsWith('4')) {
this.active = this.getDefaultSettings();
this.active.version = this.version;
await this.save();
return this.active;
}
// if version number is undefined, we make it defined
// this should only happen on first extension initialization
if (!this.active.version) {
this.active.version = this.version;
await this.save();
return this.active;
}
// check if extension has been updated. If not, return settings as they were retrieved
if (this.active.version === this.version) {
this.logger?.log('info', 'settings', "[Settings::init] extension was saved with current version of ultrawidify. Returning object as-is.");
return this.active;
}
// This means extension update happened.
// btw fun fact — we can do version rollbacks, which might come in handy while testing
this.active.version = this.version;
// if extension has been updated, update existing settings with any options added in the
// new version. In addition to that, we remove old keys that are no longer used.
const patched = ObjectCopy.addNew(settings, this.default);
this.logger?.log('info', 'settings',"[Settings.init] Results from ObjectCopy.addNew()?", patched, "\n\nSettings from storage", settings, "\ndefault?", this.default);
if (patched) {
this.active = patched;
}
// in case settings in previous version contained a fucky wucky, we overwrite existing settings with a patch
this.applySettingsPatches(oldVersion, ExtensionConfPatch);
// set 'whatsNewChecked' flag to false when updating, always
this.active.whatsNewChecked = false;
// update settings version to current
this.active.version = this.version;
await this.save();
return this.active;
}
async get() {
let ret;
if (currentBrowser.firefox) {
ret = await browser.storage.local.get('uwSettings');
} else if (currentBrowser.chrome) {
ret = await new Promise( (resolve, reject) => {
chrome.storage.local.get('uwSettings', (res) => resolve(res));
});
} else if (currentBrowser.edge) {
ret = await new Promise( (resolve, reject) => {
browser.storage.local.get('uwSettings', (res) => resolve(res));
});
}
this.logger?.log('info', 'settings', 'Got settings:', ret && ret.uwSettings && JSON.parse(ret.uwSettings));
try {
return JSON.parse(ret.uwSettings);
} catch(e) {
return undefined;
}
}
fixSitesSettings(sites) {
for (const site in sites) {
if (site === '@global') {
continue;
}
if (sites[site].mode === undefined) {
sites[site].mode = ExtensionMode.Default;
}
if (sites[site].autoar === undefined) {
sites[site].autoar = ExtensionMode.Default;
}
if (sites[site].stretch === undefined) {
sites[site].stretch = Stretch.Default;
}
if (sites[site].videoAlignment === undefined) {
sites[site].videoAlignment = VideoAlignment.Default;
}
if (sites[site].keyboardShortcutsEnabled === undefined) {
sites[site].keyboardShortcutsEnabled = ExtensionMode.Default;
}
}
}
async set(extensionConf, options) {
if (!options || !options.forcePreserveVersion) {
extensionConf.version = this.version;
}
this.fixSitesSettings(extensionConf.sites);
this.logger?.log('info', 'settings', "[Settings::set] setting new settings:", extensionConf)
if (BrowserDetect.firefox) {
return browser.storage.local.set( {'uwSettings': JSON.stringify(extensionConf)});
} else {
return chrome.storage.local.set( {'uwSettings': JSON.stringify(extensionConf)});
}
}
async setActive(activeSettings) {
this.active = activeSettings;
}
async setProp(prop, value) {
this.active[prop] = value;
}
async save(options) {
if (Debug.debug && Debug.storage) {
console.log("[Settings::save] Saving active settings:", this.active);
}
this.active.preventReload = undefined;
await this.set(this.active, options);
}
async saveWithoutReload() {
this.active.preventReload = true;
await this.set(this.active);
}
async rollback() {
this.active = await this.get();
}
getDefaultSettings() {
return JSON.parse(JSON.stringify(this.default));
}
// -----------------------------------------
// Nastavitve za posamezno stran
// Config for a given page:
//
// <hostname> : {
// status: <option> // should extension work on this site?
// arStatus: <option> // should we do autodetection on this site?
// statusEmbedded: <option> // reserved for future... maybe
// }
//
// Veljavne vrednosti za možnosti
// Valid values for options:
//
// status, arStatus, statusEmbedded:
//
// * enabled — always allow
// * basic — only allow fullscreen
// * default — allow if default is to allow, block if default is to block
// * disabled — never allow
getActionsForSite(site) {
if (!site) {
return this.active.actions;
}
if (this.active.sites[site] && this.active.sites[site].actions && this.active.sites[site].actions.length > 0) {
return this.active.sites[site].actions;
}
return this.active.actions;
}
getExtensionMode(site) {
if (!site) {
site = window.location.hostname;
if (!site) {
this.logger?.log('info', 'settings', `[Settings::canStartExtension] window.location.hostname is null or undefined: ${window.location.hostname} \nactive settings:`, this.active);
return ExtensionMode.Disabled;
}
}
try {
// if site-specific settings don't exist for the site, we use default mode:
if (! this.active.sites[site]) {
return this.getExtensionMode('@global');
}
if (this.active.sites[site].mode === ExtensionMode.Enabled) {
return ExtensionMode.Enabled;
} else if (this.active.sites[site].mode === ExtensionMode.Basic) {
return ExtensionMode.Basic;
} else if (this.active.sites[site].mode === ExtensionMode.Disabled) {
return ExtensionMode.Disabled;
} else {
if (site !== '@global') {
return this.getExtensionMode('@global');
} else {
return ExtensionMode.Disabled;
}
}
} catch(e){
this.logger?.log('error', 'settings', "[Settings.js::canStartExtension] Something went wrong — are settings defined/has init() been called?\n\nerror:", e, "\n\nSettings object:", this)
return ExtensionMode.Disabled;
}
}
canStartExtension(site) {
// returns 'true' if extension can be started on a given site. Returns false if we shouldn't run.
if (!site) {
site = window.location.hostname;
if (!site) {
this.logger?.log('info', 'settings', `[Settings::canStartExtension] window.location.hostname is null or undefined: ${window.location.hostname} \nactive settings:`, this.active);
return false;
}
}
// if (Debug.debug) {
// // let's just temporarily disable debugging while recursively calling
// // this function to get extension status on current site without duplo
// // console logs (and without endless recursion)
// Debug.debug = false;
// const cse = this.canStartExtension(site);
// Debug.debug = true;
// }
try{
// if site is not defined, we use default mode:
if (! this.active.sites[site] || this.active.sites[site].mode === ExtensionMode.Default) {
return this.active.sites['@global'].mode === ExtensionMode.Enabled;
}
if (this.active.sites['@global'].mode === ExtensionMode.Enabled) {
return this.active.sites[site].mode !== ExtensionMode.Disabled;
} else if (this.active.sites['@global'].mode === ExtensionMode.Whitelist) {
return this.active.sites[site].mode === ExtensionMode.Enabled;
} else {
return false;
}
} catch(e) {
this.logger?.log('error', 'settings', "[Settings.js::canStartExtension] Something went wrong — are settings defined/has init() been called?\nSettings object:", this);
return false;
}
}
keyboardShortcutsEnabled(site) {
if (!site) {
site = window.location.hostname;
}
if (!site) {
return false;
}
try {
if (!this.active.sites[site]
|| this.active.sites[site].keyboardShortcutsEnabled === undefined
|| this.active.sites[site].keyboardShortcutsEnabled === ExtensionMode.Default) {
return this.keyboardShortcutsEnabled('@global');
} else {
return this.active.sites[site].keyboardShortcutsEnabled === ExtensionMode.Enabled;
}
} catch (e) {
this.logger?.log('info', 'settings',"[Settings.js::keyboardDisabled] something went wrong:", e);
return false;
}
}
extensionEnabled(){
return this.active.sites['@global'] !== ExtensionMode.Disabled
}
extensionEnabledForSite(site) {
return this.canStartExtension(site);
}
canStartAutoAr(site) {
// 'site' argument is only ever used when calling this function recursively for debugging
if (!site) {
site = window.location.hostname;
if (!site) {
this.logger?.log('warn', ['settings', 'init', 'debug'], `[Settings::canStartAutoAr] No site — even window.location.hostname returned nothing!: ${window.location.hostname}`);
return false;
}
}
// if (Debug.debug) {
// let's just temporarily disable debugging while recursively calling
// this function to get extension status on current site without duplo
// console logs (and without endless recursion)
// Debug.debug = false;
// const csar = this.canStartAutoAr(site);
// Debug.debug = true;
this.logger?.log('info', ['settings', 'init', 'debug'], "[Settings::canStartAutoAr] ----------------\nCAN WE START AUTOAR ON SITE", site,
"?\n\nsettings.active.sites[site]=", this.active.sites[site], "settings.active.sites[@global]=", this.active.sites['@global'],
"\nAutoar mode (global)?", this.active.sites['@global'].autoar,
`\nAutoar mode (${site})`, this.active.sites[site] ? this.active.sites[site].autoar : '<not defined>',
// "\nCan autoar be started?", csar
);
// }
// if site is not defined, we use default mode:
if (! this.active.sites[site]) {
this.logger?.log('info', ['settings', 'aard', 'init', 'debug'], "[Settings::canStartAutoAr] Settings not defined for this site, returning defaults.", site, this.active.sites[site], this.active.sites);
return this.active.sites['@global'].autoar === ExtensionMode.Enabled;
}
if (this.active.sites['@global'].autoar === ExtensionMode.Enabled) {
this.logger?.log('info', ['settings', 'aard', 'init', 'debug'], `[Settings::canStartAutoAr] Aard is enabled by default. Extension can run unless disabled for this site.`, this.active.sites[site].autoar);
return this.active.sites[site].autoar !== ExtensionMode.Disabled;
} else if (this.active.sites['@global'].autoar === ExtensionMode.Whitelist) {
this.logger?.log('info', ['settings', 'init', 'debug'], "canStartAutoAr — can(not) start aard because extension is in whitelist mode, and this site is (not) equal to", ExtensionMode.Enabled)
return this.active.sites[site].autoar === ExtensionMode.Enabled;
} else {
this.logger?.log('info', ['settings', 'init', 'debug'], "canStartAutoAr — cannot start aard because extension is globally disabled")
return false;
}
}
getDefaultOption(option) {
const allDefault = {
mode: ExtensionMode.Default,
autoar: ExtensionMode.Default,
autoarFallback: ExtensionMode.Default,
stretch: Stretch.Default,
videoAlignment: VideoAlignment.Default,
};
if (!option || allDefault[option] === undefined) {
return allDefault;
}
return allDefault[option];
}
getDefaultAr(site) {
// site = this.getSiteSettings(site);
// if (site.defaultAr) {
// return site.defaultAr;
// }
return this.active.miscSettings.defaultAr;
}
getDefaultStretchMode(site) {
if (site && (this.active.sites[site]?.stretch ?? Stretch.Default) !== Stretch.Default) {
return this.active.sites[site].stretch;
}
return this.active.sites['@global'].stretch;
}
getDefaultCropPersistenceMode(site) {
if (site && (this.active.sites[site]?.cropModePersistence ?? Stretch.Default) !== Stretch.Default) {
return this.active.sites[site].cropModePersistence;
}
// persistence mode thing is missing from settings by default
return this.active.sites['@global'].cropModePersistence || CropModePersistence.Disabled;
}
getDefaultVideoAlignment(site) {
if ( (this.active.sites[site]?.videoAlignment ?? VideoAlignment.Default) !== VideoAlignment.Default) {
return this.active.sites[site].videoAlignment;
}
return this.active.sites['@global'].videoAlignment;
}
}
export default Settings;
if(process.env.CHANNEL !== 'stable'){
console.info("Settings loaded");
}

3
src/ext/lib/Util.js Normal file
View File

@ -0,0 +1,3 @@
export async function sleep(timeout) {
return new Promise( (resolve, reject) => setTimeout(() => resolve(), timeout));
}

View File

@ -0,0 +1,966 @@
import Debug from '../../conf/Debug';
import EdgeDetect from './edge-detect/EdgeDetect';
import EdgeStatus from './edge-detect/enums/EdgeStatusEnum';
import EdgeDetectPrimaryDirection from './edge-detect/enums/EdgeDetectPrimaryDirectionEnum';
import EdgeDetectQuality from './edge-detect/enums/EdgeDetectQualityEnum';
import GuardLine from './GuardLine';
// import DebugCanvas from './DebugCanvas';
import VideoAlignment from '../../../common/enums/video-alignment.enum';
import AspectRatio from '../../../common/enums/aspect-ratio.enum';
import {sleep} from '../../lib/Util';
import BrowserDetect from '../../conf/BrowserDetect';
class ArDetector {
constructor(videoData){
this.logger = videoData.logger;
this.conf = videoData;
this.video = videoData.video;
this.settings = videoData.settings;
this.setupTimer = null;
this.sampleCols = [];
this.canFallback = true;
this.fallbackMode = false;
this.blackLevel = this.settings.active.arDetect.blackbar.blackLevel;
this.arid = (Math.random()*100).toFixed();
// ar detector starts in this state. running main() sets both to false
this._halted = true;
this._exited = true;
// we can tick manually, for debugging
this._manualTicks = false;
this._nextTick = false;
this.canDoFallbackMode = false;
this.drmNotificationShown = false;
this.logger.log('info', 'init', `[ArDetector::ctor] creating new ArDetector. arid: ${this.arid}`);
}
setManualTick(manualTick) {
this._manualTicks = manualTick;
}
tick() {
this._nextTick = true;
}
init(){
this.logger.log('info', 'init', `[ArDetect::init] <@${this.arid}> Initializing autodetection.`);
try {
if (this.settings.canStartAutoAr()) {
this.setup();
} else {
throw "Settings prevent autoar from starting"
}
} catch (e) {
this.logger.log('error', 'init', `%c[ArDetect::init] <@${this.arid}> Initialization failed.`, _ard_console_stop, e);
}
}
destroy(){
this.logger.log('info', 'init', `%c[ArDetect::destroy] <@${this.arid}> Destroying aard.`, _ard_console_stop, e);
// this.debugCanvas.destroy();
this.stop();
}
setup(cwidth, cheight){
this.logger.log('info', 'init', `[ArDetect::setup] <@${this.arid}> Starting autodetection setup.`);
//
// [-1] check for zero-width and zero-height videos. If we detect this, we kick the proverbial
// can some distance down the road. This problem will prolly fix itself soon. We'll also
// not do any other setup until this issue is fixed
//
if(this.video.videoWidth === 0 || this.video.videoHeight === 0 ){
this.logger.log('warn', 'debug', `[ArDetect::setup] <@${this.arid}> This video has zero width or zero height. Dimensions: ${this.video.videoWidth} × ${this.video.videoHeight}`);
this.scheduleInitRestart();
return;
}
//
// [0] initiate "dependencies" first
//
this.guardLine = new GuardLine(this);
this.edgeDetector = new EdgeDetect(this);
// this.debugCanvas = new DebugCanvas(this);
//
// [1] initiate canvases
//
if (!cwidth) {
cwidth = this.settings.active.arDetect.canvasDimensions.sampleCanvas.width;
cheight = this.settings.active.arDetect.canvasDimensions.sampleCanvas.height;
}
if (this.canvas) {
this.canvas.remove();
}
if (this.blackframeCanvas) {
this.blackframeCanvas.remove();
}
// things to note: we'll be keeping canvas in memory only.
this.canvas = document.createElement("canvas");
this.canvas.width = cwidth;
this.canvas.height = cheight;
this.blackframeCanvas = document.createElement("canvas");
this.blackframeCanvas.width = this.settings.active.arDetect.canvasDimensions.blackframeCanvas.width;
this.blackframeCanvas.height = this.settings.active.arDetect.canvasDimensions.blackframeCanvas.height;
this.context = this.canvas.getContext("2d");
this.blackframeContext = this.blackframeCanvas.getContext("2d");
// do setup once
// tho we could do it for every frame
this.canvasScaleFactor = cheight / this.video.videoHeight;
//
// [2] determine places we'll use to sample our main frame
//
var ncol = this.settings.active.arDetect.sampling.staticCols;
var nrow = this.settings.active.arDetect.sampling.staticRows;
var colSpacing = this.canvas.width / ncol;
var rowSpacing = (this.canvas.height << 2) / nrow;
this.sampleLines = [];
this.sampleCols = [];
for(var i = 0; i < ncol; i++){
if(i < ncol - 1)
this.sampleCols.push(Math.round(colSpacing * i));
else{
this.sampleCols.push(Math.round(colSpacing * i) - 1);
}
}
for(var i = 0; i < nrow; i++){
if(i < ncol - 5)
this.sampleLines.push(Math.round(rowSpacing * i));
else{
this.sampleLines.push(Math.round(rowSpacing * i) - 4);
}
}
//
// [3] detect if we're in the fallback mode and reset guardline
//
if (this.fallbackMode) {
this.logger.log('warn', 'debug', `[ArDetect::setup] <@${this.arid}> WARNING: CANVAS RESET DETECTED/we're in fallback mode - recalculating guardLine`, "background: #000; color: #ff2");
// blackbar, imagebar
this.guardLine.reset();
}
//
// [4] see if browser supports "fallback mode" by drawing a small portion of our window
//
try {
this.blackframeContext.drawWindow(window,0, 0, this.blackframeCanvas.width, this.blackframeCanvas.height, "rgba(0,0,128,1)");
this.canDoFallbackMode = true;
} catch (e) {
this.canDoFallbackMode = false;
}
//
// [5] do other things setup needs to do
//
this.detectionTimeoutEventCount = 0;
this.resetBlackLevel();
// if we're restarting ArDetect, we need to do this in order to force-recalculate aspect ratio
this.conf.resizer.setLastAr({type: AspectRatio.Automatic, ratio: this.getDefaultAr()});
this.canvasImageDataRowLength = cwidth << 2;
this.noLetterboxCanvasReset = false;
if (this.settings.canStartAutoAr() ) {
this.start();
}
if(Debug.debugCanvas.enabled){
// this.debugCanvas.init({width: cwidth, height: cheight});
// DebugCanvas.draw("test marker","test","rect", {x:5, y:5}, {width: 5, height: 5});
}
this.conf.arSetupComplete = true;
}
start() {
if (this.settings.canStartAutoAr()) {
this.logger.log('info', 'debug', `"%c[ArDetect::start] <@${this.arid}> Starting automatic aspect ratio detection`, _ard_console_start);
} else {
this.logger.log('warn', 'debug', `"%c[ArDetect::start] <@${this.arid}> Wanted to start automatic aspect ratio detection, but settings don't allow that. Aard won't be started.`, _ard_console_change);
return;
}
if (this.conf.resizer.lastAr.type === AspectRatio.Automatic) {
// ensure first autodetection will run in any case
this.conf.resizer.setLastAr({type: AspectRatio.Automatic, ratio: this.getDefaultAr()});
}
// launch main() if it's currently not running:
this.main();
// automatic detection starts halted. If halted=false when main first starts, extension won't run
// this._paused is undefined the first time we run this function, which is effectively the same thing
// as false. Still, we'll explicitly fix this here.
this._paused = false;
this._halted = false;
this._paused = false;
}
unpause() {
// pause only if we were running before. Don't pause if we aren't running
// (we are running when _halted is neither true nor undefined)
if (this._paused && this._halted === false) {
this._paused = true;
}
}
pause() {
// pause only if we were running before. Don't pause if we aren't running
// (we are running when _halted is neither true nor undefined)
if (this._halted === false) {
this._paused = true;
}
}
stop(){
this.logger.log('info', 'debug', `"%c[ArDetect::stop] <@${this.arid}> Stopping automatic aspect ratio detection`, _ard_console_stop);
this._halted = true;
// this.conf.resizer.setArLastAr();
}
async main() {
if (this._paused) {
// unpause if paused
this._paused = false;
return; // main loop still keeps executing. Return is needed to avoid a million instances of autodetection
}
if (!this._halted) {
// we are already running, don't run twice
// this would have handled the 'paused' from before, actually.
return;
}
let exitedRetries = 10;
while (!this._exited && exitedRetries --> 0) {
this.logger.log('warn', 'debug', `[ArDetect::main] <@${this.arid}> We are trying to start another instance of autodetection on current video, but the previous instance hasn't exited yet. Waiting for old instance to exit ...`);
await sleep(this.settings.active.arDetect.timers.tickrate);
}
if (!this._exited) {
this.logger.log('error', 'debug', `[ArDetect::main] <@${this.arid}> Previous instance didn't exit in time. Not starting a new one.`);
return;
}
this.logger.log('info', 'debug', `%c[ArDetect::main] <@${this.arid}> Previous instance didn't exit in time. Not starting a new one.`);
// we need to unhalt:
this._halted = false;
this._exited = false;
// set initial timestamps so frame check will trigger the first time we run the loop
let lastFrameCheckStartTime = Date.now() - (this.settings.active.arDetect.timers.playing << 1);
const frameCheckTimes = new Array(10).fill(-1);
let frameCheckBufferIndex = 0;
let fcstart, fctime;
while (this && !this._halted) {
// NOTE: we separated tickrate and inter-check timeouts so that when video switches
// state from 'paused' to 'playing', we don't need to wait for the rest of the longer
// paused state timeout to finish.
if ( (!this._manualTicks && this.canTriggerFrameCheck(lastFrameCheckStartTime)) || this._nextTick) {
this._nextTick = false;
lastFrameCheckStartTime = Date.now();
fcstart = performance.now();
try {
this.frameCheck();
} catch (e) {
this.logger.log('error', 'debug', `%c[ArDetect::main] <@${this.arid}> Frame check failed:`, "color: #000, background: #f00", e);
}
if (Debug.performanceMetrics) {
fctime = performance.now() - fcstart;
frameCheckTimes[frameCheckBufferIndex % frameCheckTimes.length] = fctime;
this.conf.pageInfo.sendPerformanceUpdate({frameCheckTimes: frameCheckTimes, lastFrameCheckTime: fctime});
++frameCheckBufferIndex;
}
}
await sleep(this.settings.active.arDetect.timers.tickrate);
}
this.logger.log('info', 'debug', `%c[ArDetect::main] <@${this.arid}> Main autodetection loop exited. Halted? ${this._halted}`, _ard_console_stop);
this._exited = true;
}
canTriggerFrameCheck(lastFrameCheckStartTime) {
if (this._paused) {
return false;
}
if (this.video.ended || this.video.paused){
// we slow down if ended or pausing. Detecting is pointless.
// we don't stop outright in case seeking happens during pause/after video was
// ended and video gets into 'playing' state again
return Date.now() - lastFrameCheckStartTime > this.settings.active.arDetect.timers.paused;
}
if (this.video.error){
// če je video pavziran, še vedno skušamo zaznati razmerje stranic - ampak bolj poredko.
// if the video is paused, we still do autodetection. We just do it less often.
return Date.now() - lastFrameCheckStartTime > this.settings.active.arDetect.timers.error;
}
return Date.now() - lastFrameCheckStartTime > this.settings.active.arDetect.timers.playing;
}
isRunning(){
return ! (this._halted || this._paused || this._exited);
}
scheduleInitRestart(timeout, force_reset){
if(! timeout){
timeout = 100;
}
// don't allow more than 1 instance
if(this.setupTimer){
clearTimeout(this.setupTimer);
}
var ths = this;
this.setupTimer = setTimeout(function(){
ths.setupTimer = null;
try{
ths.main();
} catch(e) {
this.logger('error', 'debug', `[ArDetector::scheduleInitRestart] <@${this.arid}> Failed to start main(). Error:`,e);
}
ths = null;
},
timeout
);
}
//#region helper functions (general)
attachCanvas(canvas){
if(this.attachedCanvas)
this.attachedCanvas.remove();
// todo: place canvas on top of the video instead of random location
canvas.style.position = "absolute";
canvas.style.left = "200px";
canvas.style.top = "1200px";
canvas.style.zIndex = 10000;
document.getElementsByTagName("body")[0]
.appendChild(canvas);
}
canvasReadyForDrawWindow(){
this.logger.log('info', 'debug', `%c[ArDetect::canvasReadyForDrawWindow] <@${this.arid}> canvas is ${this.canvas.height === window.innerHeight ? '' : 'NOT '}ready for drawWindow(). Canvas height: ${this.canvas.height}px; window inner height: ${window.innerHeight}px.`)
return this.canvas.height == window.innerHeight
}
getTimeout(baseTimeout, startTime){
var execTime = (performance.now() - startTime);
return baseTimeout;
}
//#endregion
getDefaultAr() {
const ratio = this.video.videoWidth / this.video.videoHeight;
if (isNaN(ratio)) {
return undefined;
}
return ratio;
}
calculateArFromEdges(edges) {
// if we don't specify these things, they'll have some default values.
if(edges.top === undefined){
edges.top = 0;
edges.bottom = 0;
edges.left = 0; // RESERVED FOR FUTURE — CURRENTLY UNUSED
edges.right = 0; // THIS FUNCTION CAN PRESENTLY ONLY HANDLE LETTERBOX
}
let letterbox = edges.top + edges.bottom;
if (! this.fallbackMode) {
// Since video is stretched to fit the canvas, we need to take that into account when calculating target
// aspect ratio and correct our calculations to account for that
const fileAr = this.video.videoWidth / this.video.videoHeight;
const canvasAr = this.canvas.width / this.canvas.height;
let widthCorrected;
if (edges.top && edges.bottom) {
// in case of letterbox, we take canvas height as canon and assume width got stretched or squished
if (fileAr != canvasAr) {
widthCorrected = this.canvas.height * fileAr;
} else {
widthCorrected = this.canvas.width;
}
return widthCorrected / (this.canvas.height - letterbox);
}
} else {
// fallback mode behaves a wee bit differently
let zoomFactor = 1;
// there's stuff missing from the canvas. We need to assume canvas' actual height is bigger by a factor x, where
// x = [video.zoomedHeight] / [video.unzoomedHeight]
//
// letterbox also needs to be corrected:
// letterbox += [video.zoomedHeight] - [video.unzoomedHeight]
var vbr = this.video.getBoundingClientRect();
zoomFactor = vbr.height / this.video.clientHeight;
letterbox += vbr.height - this.video.clientHeight;
var trueHeight = this.canvas.height * zoomFactor - letterbox;
if(edges.top > 1 && edges.top <= this.settings.active.arDetect.fallbackMode.noTriggerZonePx ){
this.logger.log('info', 'arDetect', `%c[ArDetect::calculateArFromEdges] <@${this.arid}> Edge is in the no-trigger zone. Aspect ratio change is not triggered.`)
return;
}
// varnostno območje, ki naj ostane črno (da lahko v fallback načinu odkrijemo ožanje razmerja stranic).
// x2, ker je safetyBorderPx definiran za eno stran.
// safety border so we can detect aspect ratio narrowing (21:9 -> 16:9).
// x2 because safetyBorderPx is for one side.
trueHeight += (this.settings.active.arDetect.fallbackMode.safetyBorderPx << 1);
return this.canvas.width * zoomFactor / trueHeight;
}
}
processAr(trueAr){
this.detectedAr = trueAr;
// poglejmo, če se je razmerje stranic spremenilo
// check if aspect ratio is changed:
var lastAr = this.conf.resizer.getLastAr();
if (lastAr.type === AspectRatio.Automatic && lastAr.ratio !== null && lastAr.ratio !== undefined){
// spremembo lahko zavrnemo samo, če uporabljamo avtomatski način delovanja in če smo razmerje stranic
// že nastavili.
//
// we can only deny aspect ratio changes if we use automatic mode and if aspect ratio was set from here.
var arDiff = trueAr - lastAr.ar;
if (arDiff < 0)
arDiff = -arDiff;
var arDiff_percent = arDiff / trueAr;
// ali je sprememba v mejah dovoljenega? Če da -> fertik
// is ar variance within acceptable levels? If yes -> we done
this.logger.log('info', 'arDetect', `%c[ArDetect::processAr] <@${this.arid}> New aspect ratio varies from the old one by this much:\n`,"color: #aaf","old Ar", lastAr.ar, "current ar", trueAr, "arDiff (absolute):",arDiff,"ar diff (relative to new ar)", arDiff_percent);
if (arDiff < trueAr * this.settings.active.arDetect.allowedArVariance){
this.logger.log('info', 'arDetect', `%c[ArDetect::processAr] <@${this.arid}> Aspect ratio change denied — diff %: ${arDiff_percent}`, "background: #740; color: #fa2");
return;
}
this.logger.log('info', 'arDetect', `%c[ArDetect::processAr] <@${this.arid}> aspect ratio change accepted — diff %: ${arDiff_percent}`, "background: #153; color: #4f9");
}
this.logger.log('info', 'debug', `%c[ArDetect::processAr] <@${this.arid}> Triggering aspect ratio change. New aspect ratio: ${trueAr}`, _ard_console_change);
this.conf.resizer.updateAr({type: AspectRatio.Automatic, ratio: trueAr}, {type: AspectRatio.Automatic, ratio: trueAr});
}
clearImageData(id) {
if (ArrayBuffer.transfer) {
ArrayBuffer.transfer(id, 0);
}
id = undefined;
}
/**
* Checks whether video we're trying to play is protected by DRM.
*
* When trying to get an image frame of a DRM-protected video in
* firefox, the method canvas.drawImage(video) will throw an exception.
*
* This doesn't happen in Chrome. As opposed to Firefox, chrome will
* simply draw a transparent black image and not tell anyone that
* anything is amiss. However, since the image is (according to my testing
* on netflix) completely transparent, this means we can determine whether
* the video is DRM-protected by looking at the alpha byte of the image.
*
* (Videos don't tend to have an alpha channel, so they're always
* completely opaque (i.e. have value of 255))
*/
hasDRM() {
return this.blackframeContext.getImageData(0,0,1,1).data[3] === 0;
}
frameCheck(){
if(! this.video){
this.logger.log('error', 'debug', `%c[ArDetect::frameCheck] <@${this.arid}> Video went missing. Destroying current instance of videoData.`);
this.conf.destroy();
return;
}
if (!this.blackframeContext) {
this.init();
}
var startTime = performance.now();
let sampleCols = this.sampleCols.slice(0);
//
// [0] blackframe tests (they also determine whether we need fallback mode)
//
try {
this.blackframeContext.drawImage(this.video, 0, 0, this.blackframeCanvas.width, this.blackframeCanvas.height);
// special browsers require special tests
if (this.hasDRM()) {
this.fallbackMode = false;
throw 'Video is protected by DRM. Autodetection cannot run here.';
}
this.fallbackMode = false;
} catch (e) {
this.logger.log('error', 'arDetect', `%c[ArDetect::frameCheck] <@${this.arid}> %c[ArDetect::frameCheck] can't draw image on canvas. ${this.canDoFallbackMode ? 'Trying canvas.drawWindow instead' : 'Doing nothing as browser doesn\'t support fallback mode.'}`, "color:#000; backgroud:#f51;", e);
// nothing to see here, really, if fallback mode isn't supported by browser
if (!this.drmNotificationShown) {
this.drmNotificationShown = true;
// if we detect Edge, we'll throw the aggressive popup
this.conf.player.showEdgeNotification();
this.conf.player.showNotification('AARD_DRM');
this.conf.resizer.setAr({type: AspectRatio.Reset});
return;
}
// in case of DRM errors, we need to prevent the execution to reach the aspec
// ratio setting part of this function
if (e === 'Video is protected by DRM. Autodetection cannot run here.') {
return;
}
if (! this.canvasReadyForDrawWindow()) {
// this means canvas needs to be resized, so we'll just re-run setup with all those new parameters
this.stop();
let newCanvasWidth = window.innerHeight * (this.video.videoWidth / this.video.videoHeight);
let newCanvasHeight = window.innerHeight;
if (this.conf.resizer.videoAlignment === VideoAlignment.Center) {
this.canvasDrawWindowHOffset = Math.round((window.innerWidth - newCanvasWidth) * 0.5);
} else if (this.conf.resizer.videoAlignment === VideoAlignment.Left) {
this.canvasDrawWindowHOffset = 0;
} else {
this.canvasDrawWindowHOffset = window.innerWidth - newCanvasWidth;
}
this.setup(newCanvasWidth, newCanvasHeight);
return;
}
// if this is the case, we'll first draw on canvas, as we'll need intermediate canvas if we want to get a
// smaller sample for blackframe check
this.fallbackMode = true;
try {
this.context.drawWindow(window, this.canvasDrawWindowHOffset, 0, this.canvas.width, this.canvas.height, "rgba(0,0,128,1)");
} catch (e) {
this.logger.log('error', 'arDetect', `%c[ArDetect::frameCheck] can't draw image on canvas with fallback mode either. This error is prolly only temporary.`, "color:#000; backgroud:#f51;", e);
return; // it's prolly just a fluke, so we do nothing special here
}
// draw blackframe sample from our main sample:
this.blackframeContext.drawImage(this.canvas, this.blackframeCanvas.width, this.blackframeCanvas.height)
this.logger.log('info', 'arDetect_verbose', `%c[ArDetect::frameCheck] canvas.drawImage seems to have worked`, "color:#000; backgroud:#2f5;");
}
const bfanalysis = this.blackframeTest();
if (bfanalysis.isBlack) {
// we don't do any corrections on frames confirmed black
this.logger.log('info', 'arDetect_verbose', `%c[ArDetect::frameCheck] Black frame analysis suggests this frame is black or too dark. Doing nothing.`, "color: #fa3", bfanalysis);
return;
}
// if we are in fallback mode, then frame has already been drawn to the main canvas.
// if we are in normal mode though, the frame has yet to be drawn
if (!this.fallbackMode) {
this.context.drawImage(this.video, 0, 0, this.canvas.width, this.canvas.height);
}
const imageData = this.context.getImageData(0, 0, this.canvas.width, this.canvas.height).data;
if (! this.fastLetterboxPresenceTest(imageData, sampleCols) ) {
// Če ne zaznamo letterboxa, kličemo reset. Lahko, da je bilo razmerje stranic popravljeno na roke. Možno je tudi,
// da je letterbox izginil.
// If we don't detect letterbox, we reset aspect ratio to aspect ratio of the video file. The aspect ratio could
// have been corrected manually. It's also possible that letterbox (that was there before) disappeared.
this.conf.resizer.updateAr({type: AspectRatio.Automatic, ratio: this.getDefaultAr()});
this.guardLine.reset();
this.noLetterboxCanvasReset = true;
this.logger.log('info', 'arDetect_verbose', `%c[ArDetect::frameCheck] Letterbox not detected in fast test. Letterbox is either gone or we manually corrected aspect ratio. Nothing will be done.`, "color: #fa3");
this.clearImageData(imageData);
return;
}
// Če preverjamo naprej, potem moramo postaviti to vrednost nazaj na 'false'. V nasprotnem primeru se bo
// css resetiral enkrat na video/pageload namesto vsakič, ko so za nekaj časa obrobe odstranejene
// if we look further we need to reset this value back to false. Otherwise we'll only get CSS reset once
// per video/pageload instead of every time letterbox goes away (this can happen more than once per vid)
this.noLetterboxCanvasReset = false;
// poglejmo, če obrežemo preveč.
// let's check if we're cropping too much
const guardLineOut = this.guardLine.check(imageData, this.fallbackMode);
// če ni padla nobena izmed funkcij, potem se razmerje stranic ni spremenilo
// if both succeed, then aspect ratio hasn't changed.
if (!guardLineOut.imageFail && !guardLineOut.blackbarFail) {
this.logger.log('info', 'arDetect_verbose', `%c[ArDetect::frameCheck] guardLine tests were successful. (no imagefail and no blackbarfail)\n`, "color: #afa", guardLineOut);
this.clearImageData(imageData);
return;
}
// drugače nadaljujemo, našemu vzorcu stolpcev pa dodamo tiste stolpce, ki so
// kršili blackbar (če obstajajo) ter jih razvrstimo
// otherwise we continue. We add blackbar violations to the list of the cols
// we'll sample and sort them
if (guardLineOut.blackbarFail) {
sampleCols.concat(guardLineOut.offenders).sort((a, b) => a > b);
}
// if we're in fallback mode and blackbar test failed, we restore CSS and quit
// (since the new letterbox edge isn't present in our sample due to technical
// limitations)
if (this.fallbackMode && guardLineOut.blackbarFail) {
this.conf.resizer.setAr({type: AspectRatio.Automatic, ratio: this.getDefaultAr()});
this.guardLine.reset();
this.noLetterboxCanvasReset = true;
this.clearImageData(imageData);
return;
}
// će se razmerje stranic spreminja iz ožjega na širšega, potem najprej poglejmo za prisotnostjo navpičnih črnih obrob.
// če so prisotne navpične obrobe tudi na levi in desni strani, potlej obstaja možnost, da gre za logo na črnem ozadju.
// v tem primeru obstaja nevarnost, da porežemo preveč. Ker obstaja dovolj velika možnost, da bi porezali preveč, rajši
// ne naredimo ničesar.
//
// če je pillarbox zaznan v primeru spremembe iz ožjega na širše razmerje stranice, razmerje povrnemo na privzeto vrednost.
//
// If aspect ratio changes from narrower to wider, we first check for presence of pillarbox. Presence of pillarbox indicates
// a chance of a logo on black background. We could cut easily cut too much. Because there's a somewhat significant chance
// that we will cut too much, we rather avoid doing anything at all. There's gonna be a next chance.
try{
if(guardLineOut.blackbarFail || guardLineOut.imageFail){
if(this.edgeDetector.findBars(imageData, null, EdgeDetectPrimaryDirection.HORIZONTAL).status === 'ar_known'){
if(guardLineOut.blackbarFail){
this.logger.log('info', 'arDetect', `[ArDetect::frameCheck] Detected blackbar violation and pillarbox. Resetting to default aspect ratio.`);
this.conf.resizer.setAr({type: AspectRatio.Automatic, ratio: this.getDefaultAr()});
this.guardLine.reset();
}
triggerTimeout = this.getTimeout(baseTimeout, startTime);
this.scheduleFrameCheck(triggerTimeout);
this.clearImageData(imageData);
return;
}
}
} catch(e) {
this.logger.log('info', 'arDetect', `[ArDetect::frameCheck] something went wrong while checking for pillarbox. Error:\n`, e);
}
// pa poglejmo, kje se končajo črne letvice na vrhu in na dnu videa.
// let's see where black bars end.
this.sampleCols_current = sampleCols.length;
// blackSamples -> {res_top, res_bottom}
var edgePost = this.edgeDetector.findBars(imageData, sampleCols, EdgeDetectPrimaryDirection.VERTICAL, EdgeDetectQuality.IMPROVED, guardLineOut, bfanalysis);
this.logger.log('info', 'arDetect_verbose', `%c[ArDetect::frameCheck] edgeDetector returned this\n`, "color: #aaf", edgePost);
if (edgePost.status !== EdgeStatus.AR_KNOWN){
// rob ni bil zaznan, zato ne naredimo ničesar.
// no edge was detected. Let's leave things as they were
this.logger.log('info', 'arDetect_verbose', `%c[ArDetect::frameCheck] Edge wasn't detected with findBars`, "color: #fa3", edgePost, "EdgeStatus.AR_KNOWN:", EdgeStatus.AR_KNOWN);
this.clearImageData(imageData);
return;
}
var newAr = this.calculateArFromEdges(edgePost);
this.logger.log('info', 'arDetect_verbose', `%c[ArDetect::frameCheck] Triggering aspect ration change! new ar: ${newAr}`, "color: #aaf");
// we also know edges for guardline, so set them.
// we need to be mindful of fallbackMode though
// if edges are okay and not invalid, we also
// allow automatic aspect ratio correction. If edges
// are bogus, we remain aspect ratio unchanged.
try {
if (!this.fallbackMode) {
// throws error if top/bottom are invalid
this.guardLine.setBlackbar({top: edgePost.guardLineTop, bottom: edgePost.guardLineBottom});
} else {
if (this.conf.player.dimensions){
this.guardLine.setBlackbarManual({
top: this.settings.active.arDetect.fallbackMode.noTriggerZonePx,
bottom: this.conf.player.dimensions.height - this.settings.active.arDetect.fallbackMode.noTriggerZonePx - 1
},{
top: edgePost.guardLineTop + this.settings.active.arDetect.guardLine.edgeTolerancePx,
bottom: edgePost.guardLineBottom - this.settings.active.arDetect.guardLine.edgeTolerancePx
})
}
}
this.processAr(newAr);
} catch (e) {
// edges weren't gucci, so we'll just reset
// the aspect ratio to defaults
this.logger.log('error', 'arDetect', `%c[ArDetect::frameCheck] There was a problem setting blackbar. Doing nothing. Error:`, e);
try {
this.guardline.reset();
} catch (e) {
// no guardline, no bigge
}
// WE DO NOT RESET ASPECT RATIO HERE IN CASE OF PROBLEMS, CAUSES UNWARRANTED RESETS:
// (eg. here: https://www.youtube.com/watch?v=nw5Z93Yt-UQ&t=410)
//
// this.conf.resizer.setAr({type: AspectRatio.Automatic, ratio: this.getDefaultAr()});
}
this.clearImageData(imageData);
}
resetBlackLevel(){
this.blackLevel = this.settings.active.arDetect.blackbar.blackLevel;
}
blackLevelTest_full() {
}
blackframeTest() {
if (this.blackLevel === undefined) {
this.logger.log('info', 'arDetect_verbose', "[ArDetect::blackframeTest] black level undefined, resetting");
this.resetBlackLevel();
}
const rows = this.blackframeCanvas.height;
const cols = this.blackframeCanvas.width;
const pixels = rows * cols;
let cumulative_r = 0, cumulative_g = 0, cumulative_b = 0;
let max_r = 0, max_g = 0, max_b = 0;
let avg_r, avg_g, avg_b;
let var_r = 0, var_g = 0, var_b = 0;
let pixelMax = 0;
let cumulativeValue = 0;
let blackPixelCount = 0;
const bfImageData = this.blackframeContext.getImageData(0, 0, cols, rows).data;
const blackTreshold = this.blackLevel + this.settings.active.arDetect.blackbar.frameTreshold;
// we do some recon for letterbox and pillarbox. While this can't determine whether letterbox/pillarbox exists
// with sufficient level of certainty due to small sample resolution, it can still give us some hints for later
let rowMax = new Array(rows).fill(0);
let colMax = new Array(cols).fill(0);
let r, c;
for (let i = 0; i < bfImageData.length; i+= 4) {
pixelMax = Math.max(bfImageData[i], bfImageData[i+1], bfImageData[i+2]);
bfImageData[i+3] = pixelMax;
if (pixelMax < blackTreshold) {
if (pixelMax < this.blackLevel) {
this.blackLevel = pixelMax;
}
blackPixelCount++;
} else {
cumulativeValue += pixelMax;
cumulative_r += bfImageData[i];
cumulative_g += bfImageData[i+1];
cumulative_b += bfImageData[i+2];
max_r = max_r > bfImageData[i] ? max_r : bfImageData[i];
max_g = max_g > bfImageData[i+1] ? max_g : bfImageData[i+1];
max_b = max_b > bfImageData[i+2] ? max_b : bfImageData[i+2];
}
r = ~~(i/rows);
c = i % cols;
if (pixelMax > rowMax[r]) {
rowMax[r] = pixelMax;
}
if (pixelMax > colMax[c]) {
colMax[c] = colMax;
}
}
max_r = 1 / (max_r || 1);
max_g = 1 / (max_g || 1);
max_b = 1 / (max_b || 1);
const imagePixels = pixels - blackPixelCount;
// calculate averages and normalize them
avg_r = (cumulative_r / imagePixels) * max_r;
avg_g = (cumulative_g / imagePixels) * max_g;
avg_b = (cumulative_b / imagePixels) * max_b;
// second pass for color variance
for (let i = 0; i < bfImageData.length; i+= 4) {
if (bfImageData[i+3] >= this.blackLevel) {
var_r += Math.abs(avg_r - bfImageData[i] * max_r);
var_g += Math.abs(avg_g - bfImageData[i+1] * max_g);
var_b += Math.abs(avg_b - bfImageData[i+1] * max_b);
}
}
const hasSufficientVariance = Math.abs(var_r - var_g) / Math.max(var_r, var_g, 1) > this.settings.active.arDetect.blackframe.sufficientColorVariance
|| Math.abs(var_r - var_b) / Math.max(var_r, var_b, 1) > this.settings.active.arDetect.blackframe.sufficientColorVariance
|| Math.abs(var_b - var_g) / Math.max(var_b, var_g, 1) > this.settings.active.arDetect.blackframe.sufficientColorVariance
let isBlack = (blackPixelCount/(cols * rows) > this.settings.active.arDetect.blackframe.blackPixelsCondition);
if (! isBlack) {
if (hasSufficientVariance) {
isBlack = cumulativeValue < this.settings.active.arDetect.blackframe.cumulativeThresholdLax;
} else {
isBlack = cumulativeValue < this.settings.active.arDetect.blackframe.cumulativeThresholdStrict;
}
}
if (Debug.debug) {
return {
isBlack: isBlack,
blackPixelCount: blackPixelCount,
blackPixelRatio: (blackPixelCount/(cols * rows)),
cumulativeValue: cumulativeValue,
hasSufficientVariance: hasSufficientVariance,
blackLevel: this.blackLevel,
variances: {
raw: {
r: var_r, g: var_g, b: var_b
},
relative: {
rg: Math.abs(var_r - var_g) / Math.max(var_r, var_g, 1),
rb: Math.abs(var_r - var_b) / Math.max(var_r, var_b, 1),
gb: Math.abs(var_b - var_g) / Math.max(var_b, var_g, 1),
},
relativePercent: {
rg: Math.abs(var_r - var_g) / Math.max(var_r, var_g, 1) / this.settings.active.arDetect.blackframe.sufficientColorVariance,
rb: Math.abs(var_r - var_b) / Math.max(var_r, var_b, 1) / this.settings.active.arDetect.blackframe.sufficientColorVariance,
gb: Math.abs(var_b - var_g) / Math.max(var_b, var_g, 1) / this.settings.active.arDetect.blackframe.sufficientColorVariance,
},
varianceLimit: this.settings.active.arDetect.blackframe.sufficientColorVariance,
},
cumulativeValuePercent: cumulativeValue / (hasSufficientVariance ? this.settings.active.arDetect.blackframe.cumulativeThresholdLax : this.settings.active.arDetect.blackframe.cumulativeThresholdStrict),
rowMax: rowMax,
colMax: colMax,
};
}
return {
isBlack: isBlack,
rowMax: rowMax,
colMax: colMax,
};
}
fastLetterboxPresenceTest(imageData, sampleCols) {
// fast test to see if aspect ratio is correct.
// returns 'true' if presence of letterbox is possible.
// returns 'false' if we found a non-black edge pixel.
// If we detect anything darker than blackLevel, we modify blackLevel to the new lowest value
const rowOffset = this.canvas.width * (this.canvas.height - 1);
let currentMin = 255, currentMax = 0, colOffset_r, colOffset_g, colOffset_b, colOffset_rb, colOffset_gb, colOffset_bb, blthreshold = this.settings.active.arDetect.blackbar.threshold;
// detect black level. if currentMax comes above blackbar + blackbar threshold, we know we aren't letterboxed
for (var i = 0; i < sampleCols.length; ++i){
colOffset_r = sampleCols[i] << 2;
colOffset_g = colOffset_r + 1;
colOffset_b = colOffset_r + 2;
colOffset_rb = colOffset_r + rowOffset;
colOffset_gb = colOffset_g + rowOffset;
colOffset_bb = colOffset_b + rowOffset;
currentMax = Math.max(
imageData[colOffset_r], imageData[colOffset_g], imageData[colOffset_b],
// imageData[colOffset_rb], imageData[colOffset_gb], imageData[colOffset_bb],
currentMax
);
if (currentMax > this.blackLevel + blthreshold) {
// we search no further
if (currentMin < this.blackLevel) {
this.blackLevel = currentMin;
}
return false;
}
currentMin = Math.min(
currentMax,
currentMin
);
}
if (currentMin < this.blackLevel)
this.blackLevel = currentMin
return true;
}
}
var _ard_console_stop = "background: #000; color: #f41";
var _ard_console_start = "background: #000; color: #00c399";
var _ard_console_change = "background: #000; color: #ff8";
export default ArDetector;

View File

@ -0,0 +1,129 @@
class DebugCanvas {
constructor(ardConf){
this.conf = ardConf;
this.targetWidth = 1280;
this.targetHeight = 720;
this.targetOffsetTop = 1080;
this.targetOffsetLeft = 100;
}
init(canvasSize, canvasPosition, targetCanvasSize) {
console.log("initiating DebugCanvas")
var body = document.getElementsByTagName('body')[0];
if(!canvasPosition){
canvasPosition = {
top: 1200,
left: 800
}
}
if(!this.canvas){
this.canvas = document.createElement("canvas");
body.appendChild(this.canvas);
}
if(targetCanvasSize){
this.targetWidth = targetCanvasSize.width;
this.targetHeight = targetCanvasSize.height;
}
this.canvas.style.position = "absolute";
this.canvas.style.left = `${canvasPosition.left}px`;
this.canvas.style.top = `${canvasPosition.top}px`;
this.canvas.style.zIndex = 10002;
// this.canvas.id = "uw_debug_canvas";
this.context = this.canvas.getContext("2d");
this.canvas.width = canvasSize.width;
this.canvas.height = canvasSize.height;
this.calculateCanvasZoom();
console.log("debug canvas is:", this.canvas, "context:", this.context)
}
calculateCanvasZoom(){
var canvasZoom = this.targetWidth / this.canvas.width;
var translateX = (this.canvas.width - this.targetWidth)/2;
var translateY = (this.canvas.height - this.targetHeight)/2;
this.canvas.style.transform = `scale(${canvasZoom},${canvasZoom}) translateX(${translateX}px) translateY(${translateY}px)`;
}
destroy(){
if(this.canvas)
this.canvas.remove();
}
setBuffer(buffer) {
// this.imageBuffer = buffer.splice(0);
this.imageBuffer = new Uint8ClampedArray(buffer);
}
trace(arrayIndex, colorClass) {
this.imageBuffer[arrayIndex ] = colorClass.colorRgb[0];
this.imageBuffer[arrayIndex+1] = colorClass.colorRgb[1];
this.imageBuffer[arrayIndex+2] = colorClass.colorRgb[2];
}
update() {
var start = performance.now();
try{
if(this.context && this.imageBuffer instanceof Uint8ClampedArray){
try{
var idata = new ImageData(this.imageBuffer, this.canvas.width, this.canvas.height);
} catch (ee) {
console.log("[DebugCanvas.js::update] can't create image data. Trying to correct canvas size. Error was:", ee);
this.canvas.width = this.conf.canvas.width;
this.canvas.height = this.conf.canvas.height;
this.calculateCanvasZoom();
// this.context = this.canvas.getContext("2d");
var idata = new ImageData(this.imageBuffer, this.canvas.width, this.canvas.height);
}
this.putImageData(this.context, idata, 0, 0, 0, 0, this.canvas.width, this.canvas.height);
}
} catch(e) {
console.log("[DebugCanvas.js::update] updating canvas failed.", e);
}
console.log("[DebugCanvas.js::update] update took", (performance.now() - start), "ms.");
}
putImageData(ctx, imageData, dx, dy, dirtyX, dirtyY, dirtyWidth, dirtyHeight) {
var data = imageData.data;
var height = imageData.height;
var width = imageData.width;
dirtyX = dirtyX || 0;
dirtyY = dirtyY || 0;
dirtyWidth = dirtyWidth !== undefined? dirtyWidth: width;
dirtyHeight = dirtyHeight !== undefined? dirtyHeight: height;
var limitBottom = Math.min(dirtyHeight, height);
var limitRight = Math.min(dirtyWidth, width);
for (var y = dirtyY; y < limitBottom; y++) {
for (var x = dirtyX; x < limitRight; x++) {
var pos = y * width + x;
ctx.fillStyle = 'rgba(' + data[pos*4+0]
+ ',' + data[pos*4+1]
+ ',' + data[pos*4+2]
+ ',' + (data[pos*4+3]/255) + ')';
ctx.fillRect(x + dx, y + dy, 1, 1);
}
}
}
}
DebugCanvasClasses = {
VIOLATION: {color: '#ff0000', colorRgb: [255, 0, 0], text: 'violation (general)'},
WARN: {color: '#d0d039', colorRgb: [208, 208, 57], text: 'lesser violation (general)'},
GUARDLINE_BLACKBAR: {color: '#3333FF', colorRgb: [51, 51, 255], text: 'guardline/blackbar (expected value)'},
GUARDLINE_IMAGE: {color: '#000088', colorRgb: [0, 0, 136], text: 'guardline/image (expected value)'},
EDGEDETECT_ONBLACK: {color: '#444444', colorRgb: [68, 68, 68], text: 'edge detect - perpendicular (to edge)'},
EDGEDETECT_CANDIDATE: {color: '#FFFFFF', colorRgb: [255, 255, 255], text: 'edge detect - edge candidate'},
EDGEDETECT_CANDIDATE_SECONDARY: {color: '#OOOOOO', colorRgb: [0, 0, 0], text: 'edge detect - edge candidate, but for when candidate is really bright'},
EDGEDETECT_BLACKBAR: {color: '#07ac93', colorRgb: [7, 172, 147], text: 'edge detect - parallel, black test'},
EDGEDETECT_IMAGE: {color: '#046c5c', colorRgb: [4, 108, 92], text: 'edge detect - parallel, image test'}
}

View File

@ -0,0 +1,305 @@
import Debug from '../../conf/Debug';
class GuardLine {
// ardConf — reference to ArDetector that has current GuardLine instance
constructor(ardConf){
this.blackbar = {top: undefined, bottom: undefined};
this.imageBar = {top: undefined, bottom: undefined};
this.conf = ardConf;
this.settings = ardConf.settings;
}
reset() {
this.blackbar = {top: undefined, bottom: undefined};
this.imageBar = {top: undefined, bottom: undefined};
}
setBlackbarManual(blackbarConf, imagebarConf){
// ni lepo uporabljat tega, ampak pri fallback mode nastavljamo blackbar stuff na roke
// it's not nice to use this, but we're setting these values manually in fallbackMode
if (blackbarConf) {
this.blackbar = blackbarConf;
}
if (imagebarConf) {
this.imageBar = imagebarConf;
}
}
setBlackbar(bbconf){
var bbTop = bbconf.top - this.settings.active.arDetect.guardLine.edgeTolerancePx;
var bbBottom = bbconf.bottom + this.settings.active.arDetect.guardLine.edgeTolerancePx;
// to odstrani vse neveljavne nastavitve in vse možnosti, ki niso smiselne
// this removes any configs with invalid values or values that dont make sense
if (bbTop < 0 || bbBottom >= this.conf.canvas.height ){
throw {error: "INVALID_SETTINGS_IN_GUARDLINE", bbTop, bbBottom}
}
this.blackbar = {
top: bbTop,
bottom: bbBottom
}
this.imageBar = {
top: bbconf.top + 1 + this.settings.active.arDetect.guardLine.edgeTolerancePx,
bottom: bbconf.bottom - 1 - this.settings.active.arDetect.guardLine.edgeTolerancePx
}
}
check(image, fallbackMode){
// izračunaj enkrat in shrani na objekt
// calculate once and save object-instance-wide
this.blackbarThreshold = this.conf.blackLevel + this.settings.active.arDetect.blackbar.threshold;
this.imageThreshold = this.blackbarThreshold + this.settings.active.arDetect.blackbar.imageThreshold;
// dejansko testiranje
// actual checks
var guardLineResult = this.guardLineCheck(image, fallbackMode);
// Zaznali smo kršitev črnega dela, zato nam ni treba preveriti, ali je slika
// prisotna. Vemo namreč, da se je razmerje stranic zmanjšalo.
//
// blackbar violation detected. We don't even need to check for presence of image
// as aspect ratio just decreased
if(! guardLineResult.success) {
return {
blackbarFail: true,
offenders: guardLineResult.offenders,
imageFail: false
}
}
var imageCheckResult = this.imageCheck(image, fallbackMode);
return {
blackbarFail: false,
imageFail: ! imageCheckResult.success
}
}
// don't use methods below this line outside this class
guardLineCheck(image, fallbackMode){
// this test tests for whether we crop too aggressively
// if this test is passed, then aspect ratio probably didn't change from wider to narrower. However, further
// checks are needed to determine whether aspect ratio got wider.
// if this test fails, it returns a list of offending points.
// if the upper edge is null, then edge hasn't been detected before. This test is pointless, therefore it
// should succeed by default. Also need to check bottom, for cases where only one edge is known
if(! fallbackMode && (! this.blackbar.top || ! this.blackbar.bottom)) {
return { success: true };
}
var offset = parseInt(this.conf.canvas.width * this.settings.active.arDetect.guardLine.ignoreEdgeMargin) << 2;
var offenders = [];
var offenderCount = -1; // doing it this way means first offender has offenderCount==0. Ez index.
// TODO: implement logo check.
// preglejmo obe vrstici
// check both rows
if(! fallbackMode){
var edge_upper = this.blackbar.top;
var edge_lower = this.blackbar.bottom;
}
else{
// fallback mode is a bit different
edge_upper = 0;
edge_lower = this.conf.canvas.height - 1;
}
var rowStart, rowEnd;
// <<<=======| checking upper row |========>>>
rowStart = ((edge_upper * this.conf.canvas.width) << 2) + offset;
rowEnd = rowStart + ( this.conf.canvas.width << 2 ) - (offset * 2);
if (Debug.debugCanvas.enabled && Debug.debugCanvas.guardLine) {
offenderCount = this._gl_debugRowCheck(image, rowStart, rowEnd, offenders, offenderCount);
} else {
offenderCount = this._gl_rowCheck(image, rowStart, rowEnd, offenders, offenderCount);
}
// <<<=======| checking lower row |========>>>
rowStart = ((edge_lower * this.conf.canvas.width) << 2) + offset;
rowEnd = rowStart + ( this.conf.canvas.width << 2 ) - (offset * 2);
if (Debug.debugCanvas.enabled && Debug.debugCanvas.guardLine) {
offenderCount = this._gl_debugRowCheck(image, rowStart, rowEnd, offenders, offenderCount);
} else {
offenderCount = this._gl_rowCheck(image, rowStart, rowEnd, offenders, offenderCount);
}
// če nismo našli nobenih prekrškarjev, vrnemo uspeh. Drugače vrnemo seznam prekrškarjev
// vrnemo tabelo, ki vsebuje sredinsko točko vsakega prekrškarja (x + width*0.5)
//
// if we haven't found any offenders, we return success. Else we return list of offenders
// we return array of middle points of offenders (x + (width * 0.5) for every offender)
if(offenderCount == -1){
return {success: true};
}
var ret = new Array(offenders.length);
for(var o in offenders){
ret[o] = offenders[o].x + (offenders[o].width * 0.25);
}
return {success: false, offenders: ret};
}
imageCheck(image){
if(!this.imageBar.top || !this.imageBar.bottom)
return { success: false };
var offset = (this.conf.canvas.width * this.settings.active.arDetect.guardLine.ignoreEdgeMargin) << 2;
// TODO: implement logo check.
// preglejmo obe vrstici - tukaj po pravilih ne bi smeli iti prek mej platna. ne rabimo preverjati
// check both rows - by the rules and definitions, we shouldn't go out of bounds here. no need to check, then
// if(fallbackMode){
// var edge_upper = this.settings.active.arDetect.fallbackMode.noTriggerZonePx;
// var edge_lower = this.conf.canvas.height - this.settings.active.arDetect.fallbackMode.noTriggerZonePx - 1;
// }
// else{
var edge_upper = this.imageBar.top;
var edge_lower = this.imageBar.bottom;
// }
// koliko pikslov rabimo zaznati, da je ta funkcija uspe. Tu dovoljujemo tudi, da so vsi piksli na enem
// robu (eden izmed robov je lahko v celoti črn)
// how many non-black pixels we need to consider this check a success. We only need to detect enough pixels
// on one edge (one of the edges can be black as long as both aren't)
var successThreshold = (this.conf.canvas.width * this.settings.active.arDetect.guardLine.imageTestThreshold);
var rowStart, rowEnd;
// <<<=======| checking upper row |========>>>
rowStart = ((edge_upper * this.conf.canvas.width) << 2) + offset;
rowEnd = rowStart + ( this.conf.canvas.width << 2 ) - (offset * 2);
var res = false;
if(Debug.debugCanvas.enabled && Debug.debugCanvas.guardLine){
res = this._ti_debugCheckRow(image, rowStart, rowEnd, successThreshold);
} else {
res = this._ti_checkRow(image, rowStart, rowEnd,successThreshold);
}
if(res)
return {success: true};
// <<<=======| checking lower row |========>>>
rowStart = ((edge_lower * this.conf.canvas.width) << 2) + offset;
// rowEnd = rowStart + ( this.conf.canvas.width << 2 ) - (offset * 2);
if(Debug.debugCanvas.enabled && Debug.debugCanvas.guardLine){
res = this._ti_debugCheckRow(image, rowStart, rowEnd, successThreshold);
} else {
res = this._ti_checkRow(image, rowStart, rowEnd,successThreshold);
}
return {success: res};
}
// pomožne metode
// helper methods
_gl_rowCheck(image, rowStart, rowEnd, offenders, offenderCount){
var firstOffender = -1;
for(var i = rowStart; i < rowEnd; i+=4){
// we track sections that go over what's supposed to be a black line, so we can suggest more
// columns to sample
if(image[i] > this.blackbarThreshold || image[i+1] > this.blackbarThreshold || image[i+2] > this.blackbarThreshold){
if(firstOffender < 0){
firstOffender = (i - rowStart) >> 2;
offenderCount++;
offenders.push({x: firstOffender, width: 1});
}
else{
offenders[offenderCount].width++
}
}
else{
// is that a black pixel again? Let's reset the 'first offender'
firstOffender = -1;
}
}
return offenderCount;
}
_gl_debugRowCheck(image, rowStart, rowEnd, offenders, offenderCount){
var firstOffender = -1;
for(var i = rowStart; i < rowEnd; i+=4){
// we track sections that go over what's supposed to be a black line, so we can suggest more
// columns to sample
if(image[i] > this.blackbarThreshold || image[i+1] > this.blackbarThreshold || image[i+2] > this.blackbarThreshold){
this.conf.debugCanvas.trace(i, DebugCanvasClasses.VIOLATION);
if(firstOffender < 0){
firstOffender = (i - rowStart) >> 2;
offenderCount++;
offenders.push({x: firstOffender, width: 1});
}
else{
offenders[offenderCount].width++
}
}
else{
this.conf.debugCanvas.trace(i, DebugCanvasClasses.GUARDLINE_BLACKBAR);
// is that a black pixel again? Let's reset the 'first offender'
firstOffender = -1;
}
}
return offenderCount;
}
_ti_checkRow(image, rowStart, rowEnd, successThreshold) {
for(var i = rowStart; i < rowEnd; i+=4){
if(image[i] > this.imageThreshold || image[i+1] > this.imageThreshold || image[i+2] > this.imageThreshold){
if(successThreshold --<= 0){
return true;
}
}
}
return false;
}
_ti_debugCheckRow(image, rowStart, rowEnd, successThreshold) {
for(var i = rowStart; i < rowEnd; i+=4){
if(image[i] > this.imageThreshold || image[i+1] > this.imageThreshold || image[i+2] > this.imageThreshold){
this.conf.debugCanvas.trace(i, DebugCanvasClasses.GUARDLINE_IMAGE);
if(successThreshold --<= 0){
return true;
}
} else {
this.conf.debugCanvas.trace(i, DebugCanvasClasses.WARN);
}
}
return false;
}
}
export default GuardLine;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,6 @@
var EdgeDetectPrimaryDirection = Object.freeze({
VERTICAL: 0,
HORIZONTAL: 1
});
export default EdgeDetectPrimaryDirection;

View File

@ -0,0 +1,6 @@
var EdgeDetectQuality = Object.freeze({
FAST: 0,
IMPROVED: 1
});
export default EdgeDetectQuality;

View File

@ -0,0 +1,6 @@
var EdgeStatus = Object.freeze({
AR_UNKNOWN: 0,
AR_KNOWN: 1,
});
export default EdgeStatus;

View File

@ -0,0 +1,43 @@
import Debug from '../../conf/Debug';
import BrowserDetect from '../../conf/BrowserDetect';
if (process.env.CHANNEL !== 'stable'){
console.info("Loading Comms");
}
class Comms {
static async sendMessage(message){
if(BrowserDetect.firefox){
return browser.runtime.sendMessage(message);
} else {
return new Promise((resolve, reject) => {
try{
if(BrowserDetect.edge){
browser.runtime.sendMessage(message, function(response){
var r = response;
resolve(r);
});
} else {
chrome.runtime.sendMessage(message, function(response){
// Chrome/js shittiness mitigation — remove this line and an empty array will be returned
var r = response;
resolve(r);
return true;
});
}
}
catch(e){
reject(e);
}
});
}
}
}
if (process.env.CHANNEL !== 'stable'){
console.info("Comms loaded");
}
export default Comms;

View File

@ -0,0 +1,136 @@
import Debug from '../../conf/Debug';
import BrowserDetect from '../../conf/BrowserDetect';
if (process.env.CHANNEL !== 'stable'){
console.info("Loading CommsClient");
}
class CommsClient {
constructor(name, logger, commands) {
this.logger = logger;
if (BrowserDetect.firefox) {
this.port = browser.runtime.connect({name: name});
} else if (BrowserDetect.chrome) {
this.port = chrome.runtime.connect({name: name});
}
this.logger.onLogEnd(
(history) => {
this.logger.log('info', 'comms', 'Sending logging-stop-and-save to background script ...');
try {
this.port.postMessage({cmd: 'logging-stop-and-save', host: window.location.hostname, history})
} catch (e) {
this.logger.log('error', 'comms', 'Failed to send message to background script. Error:', e);
}
}
);
this._listener = m => this.processReceivedMessage(m);
this.port.onMessage.addListener(this._listener);
this.commsId = (Math.random() * 20).toFixed(0);
this.commands = commands;
}
destroy() {
if (!BrowserDetect.edge) { // edge is a very special browser made by outright morons.
this.port.onMessage.removeListener(this._listener);
}
}
subscribe(command, callback) {
if (!this.commands[command]) {
this.commands[command] = [callback];
} else {
this.commands[command].push(callback);
}
}
processReceivedMessage(message){
this.logger.log('info', 'comms', `[CommsClient.js::processMessage] <${this.commsId}> Received message from background script!`, message);
if (this.commands[message.cmd]) {
for (const c of this.commands[message.cmd]) {
c(message);
}
}
}
async sendMessage_nonpersistent(message){
message = JSON.parse(JSON.stringify(message)); // vue quirk. We should really use vue store instead
if(BrowserDetect.firefox){
return browser.runtime.sendMessage(message)
} else {
return new Promise((resolve, reject) => {
try{
if(BrowserDetect.edge){
browser.runtime.sendMessage(message, function(response){
var r = response;
resolve(r);
});
} else {
chrome.runtime.sendMessage(message, function(response){
// Chrome/js shittiness mitigation — remove this line and an empty array will be returned
var r = response;
resolve(r);
return true;
});
}
}
catch(e){
reject(e);
}
return true;
});
}
}
async requestSettings(){
this.logger.log('info', 'comms', "%c[CommsClient::requestSettings] sending request for congif!", "background: #11D; color: #aad");
var response = await this.sendMessage_nonpersistent({cmd: 'get-config'});
this.logger.log('info', 'comms', "%c[CommsClient::requestSettings] received settings response!", "background: #11D; color: #aad", response);
if(! response || response.extensionConf){
return Promise.resolve(false);
}
this.settings.active = JSON.parse(response.extensionConf);
return Promise.resolve(true);
}
async sendMessage(message) {
await this.sendMessage_nonpersistent(message);
}
registerVideo(){
this.logger.log('info', 'comms', `[CommsClient::registerVideo] <${this.commsId}>`, "Registering video for current page.");
this.port.postMessage({cmd: "has-video"});
}
sendPerformanceUpdate(message){
this.port.postMessage({cmd: 'performance-update', message: message});
}
unregisterVideo(){
this.logger.log('info', 'comms', `[CommsClient::unregisterVideo] <${this.commsId}>`, "Unregistering video for current page.");
this.port.postMessage({cmd: "noVideo"}); // ayymd
}
announceZoom(scale){
this.port.postMessage({cmd: "announce-zoom", zoom: scale});
this.registerVideo();
}
}
if (process.env.CHANNEL !== 'stable'){
console.info("CommsClient loaded");
}
export default CommsClient;

View File

@ -0,0 +1,339 @@
import Debug from '../../conf/Debug';
import BrowserDetect from '../../conf/BrowserDetect';
class CommsServer {
constructor(server) {
this.server = server;
this.logger = server.logger;
this.settings = server.settings;
this.ports = [];
this.popupPort = null;
if (BrowserDetect.firefox) {
browser.runtime.onConnect.addListener(p => this.onConnect(p));
browser.runtime.onMessage.addListener((m, sender) => this.processReceivedMessage_nonpersistent(m, sender));
} else {
chrome.runtime.onConnect.addListener(p => this.onConnect(p));
chrome.runtime.onMessage.addListener((m, sender, callback) => this.processReceivedMessage_nonpersistent(m, sender, callback));
}
// commands — functions that handle incoming messages
// functions can have the following arguments, which are,
// in this order:
// message — the message we received
// port|sender — on persistent channels, second argument is port on which the server
// listens. If the message was sent in non-persistent way, this is the
// sender script/frame/whatever of the message
// sendResponse — callback function on messages received via non-persistent channel
this.commands = {
'announce-zoom': [
(message) => {
try {
// forward message to the popup
this.popupPort.postMessage({cmd: 'set-current-zoom', zoom: message.zoom});
} catch (e) {
// if popup is closed, this will/may fail. This is okay, so we just ignore this error
}
},
],
'get-current-zoom': [
(message) => this.sendToActive(message),
],
'get-current-site': [
async (message, port) => {
port.postMessage({
cmd: 'set-current-site',
site: await this.server.getVideoTab(),
tabHostname: await this.getCurrentTabHostname()
});
},
],
'popup-set-selected-tab': [
(message) => this.server.setSelectedTab(message.selectedMenu, message.selectedSubitem),
],
'get-config': [
(message, port) => {
this.logger.log('info', 'comms', "CommsServer: received get-config. Active settings?", this.settings.active, "\n(settings:", this.settings, ")");
port.postMessage(
{cmd: "set-config", conf: this.settings.active, site: this.server.currentSite}
);
},
],
'has-video': [
(message, port) => this.server.registerVideo(port.sender),
],
'noVideo': [
(message, port) => this.server.unregisterVideo(port.sender),
],
'inject-css': [
(message, sender) => this.server.injectCss(message.cssString, sender),
],
'eject-css': [
(message, sender) => this.server.removeCss(message.cssString, sender),
],
'replace-css': [
(message, sender) => this.server.replaceCss(message.oldCssString, message.newCssString, sender),
],
'get-config': [
(message, sender, sendResponse) => {
if (BrowserDetect.firefox) {
var ret = {extensionConf: JSON.stringify(this.settings.active)};
this.logger.log('info', 'comms', "%c[CommsServer.js::processMessage_nonpersistent] Returning this:", "background-color: #11D; color: #aad", ret);
Promise.resolve(ret);
} else {
sendResponse({extensionConf: JSON.stringify(this.settings.active)});
return true;
}
}
],
'autoar-enable': [
() => {
this.settings.active.sites['@global'].autoar = "blacklist";
this.settings.save();
this.logger.log('info', 'comms', "[uw-bg] autoar set to enabled (blacklist). evidenz:", this.settings.active);
}
],
'autoar-disable': [
(message) => {
this.settings.active.sites['@global'].autoar = "disabled";
if (message.reason){
this.settings.active.arDetect.disabledReason = message.reason;
} else {
this.settings.active.arDetect.disabledReason = 'User disabled';
}
this.settings.save();
this.logger.log('info', 'comms', "[uw-bg] autoar set to disabled. evidenz:", this.settings.active);
}
],
'autoar-set-interval': [
(message) => {
this.logger.log('info', 'comms', `[uw-bg] trying to set new interval for autoAr. New interval is, ${message.timeout} ms`);
// set fairly liberal limit
var timeout = message.timeout < 4 ? 4 : message.timeout;
this.settings.active.arDetect.timer_playing = timeout;
this.settings.save();
}
],
'logging-stop-and-save': [ // TODO: possibly never used/superseded — check
(message, sender) => {
this.logger.log('info', 'comms', "Received command to stop logging and export the received input");
this.logger.addToGlobalHistory(`${message.host}::${sender?.tab?.id ?? '×'}-${sender.frameId ?? '×'}`, JSON.parse(message.history));
this.logger.finish();
}
],
'logging-save': [
(message, sender) => {
this.logger.log('info', 'comms', `Received command to save log for site ${message.host} (tabId ${sender.tab.id}, frameId ${sender.frameId}`);
this.logger.addToGlobalHistory(`${message?.host}::${sender?.tab?.id ?? '×'}-${sender?.frameId ?? '×'}`, JSON.parse(message.history));
}
]
}
}
subscribe(command, callback) {
if (!this.commands[command]) {
this.commands[command] = [callback];
} else {
this.commands[command].push(callback);
}
}
async getCurrentTabHostname() {
const activeTab = await this._getActiveTab();
if (!activeTab || activeTab.length < 1) {
this.logger.log('warn', 'comms', 'There is no active tab for some reason. activeTab:', activeTab);
}
const url = activeTab[0].url;
var hostname;
if (url.indexOf("://") > -1) { //find & remove protocol (http, ftp, etc.) and get hostname
hostname = url.split('/')[2];
}
else {
hostname = url.split('/')[0];
}
hostname = hostname.split(':')[0]; //find & remove port number
hostname = hostname.split('?')[0]; //find & remove "?"
return hostname;
}
sendToAll(message){
for(const tab of this.ports){
for(const frame in tab){
for (const port in tab[frame]) {
tab[frame][port].postMessage(message);
}
}
}
}
async _getActiveTab() {
if (BrowserDetect.firefox) {
return await browser.tabs.query({currentWindow: true, active: true});
} else {
return await new Promise( (resolve, reject) => {
chrome.tabs.query({lastFocusedWindow: true, active: true}, function (res) {
resolve(res);
return true;
});
});
}
}
// if port is NOT defined, send to all content scripts of a given frame
// if port is defined, send just to that particular script of a given frame
async sendToFrameContentScripts(message, tab, frame, port) {
if (port !== undefined) {
// note: 'port' is _not_ shadowed here.
this.ports[tab][frame][port].postMessage(message);
return;
}
for (const port in this.ports[tab][frame]) {
// note: 'port' is shadowed here!
this.ports[tab][frame][port].postMessage(message);
}
}
async sendToFrame(message, tab, frame, port) {
this.logger.log('info', 'comms', `%c[CommsServer::sendToFrame] attempting to send message to tab ${tab}, frame ${frame}`, "background: #dda; color: #11D", message);
if (isNaN(tab)) {
if (frame === '__playing') {
message['playing'] = true;
this.sendToAll(message);
return;
} else if (frame === '__all') {
this.sendToAll(message);
return;
}
[tab, frame] = frame.split('-');
}
this.logger.log('info', 'comms', `%c[CommsServer::sendToFrame] attempting to send message to tab ${tab}, frame ${frame}`, "background: #dda; color: #11D", message);
try {
this.sendToFrameContentScripts(message, tab, frame, port);
} catch (e) {
this.logger.log('error', 'comms', `%c[CommsServer::sendToFrame] Sending message failed. Reason:`, "background: #dda; color: #11D", e);
}
}
async sendToAllFrames(message, tab, port) {
for (const frame in this.ports[tab]) {
this.sendToFrameContentScripts(message, tab, frame, port);
}
}
async sendToActive(message) {
this.logger.log('info', 'comms', "%c[CommsServer::sendToActive] trying to send a message to active tab. Message:", "background: #dda; color: #11D", message);
const tabs = await this._getActiveTab();
this.logger.log('info', 'comms', "[CommsServer::_sendToActive] currently active tab(s)?", tabs);
for (const frame in this.ports[tabs[0].id]) {
this.logger.log('info', 'comms', "key?", frame, this.ports[tabs[0].id]);
}
for (const frame in this.ports[tabs[0].id]) {
this.sendToFrameContentScripts(message, tabs[0].id, frame);
}
}
onConnect(port){
// poseben primer | special case
if (port.name === 'popup-port') {
this.popupPort = port;
this.popupPort.onMessage.addListener( (m,p) => this.processReceivedMessage(m,p));
return;
}
var tabId = port.sender.tab.id;
var frameId = port.sender.frameId;
if (! this.ports[tabId]){
this.ports[tabId] = {};
}
if (! this.ports[tabId][frameId]) {
this.ports[tabId][frameId] = {};
}
this.ports[tabId][frameId][port.name] = port;
this.ports[tabId][frameId][port.name].onMessage.addListener( (m,p) => this.processReceivedMessage(m, p));
this.ports[tabId][frameId][port.name].onDisconnect.addListener( (p) => {
try {
delete this.ports[p.sender.tab.id][p.sender.frameId][port.name];
} catch (e) {
// no biggie if the thing above doesn't exist.
}
if (Object.keys(this.ports[tabId][frameId].length === 0)) {
delete this.ports[tabId][frameId];
if(Object.keys(this.ports[p.sender.tab.id]).length === 0) {
delete this.ports[tabId];
}
}
});
}
async execCmd(message, portOrSender, sendResponse) {
this.logger.log(
'info', 'comms', '[CommsServer.js::execCmd] Received message', message,
". Port/sender:", portOrSender, "sendResponse:", sendResponse, "\nThere is ", this.commands[message.cmd]?.length ?? 0,
" command(s) for action", message.cmd
);
if (this.commands[message.cmd]) {
for (const c of this.commands[message.cmd]) {
try {
await c(message, portOrSender, sendResponse);
} catch (e) {
this.logger.log('error', 'debug', "[CommsServer.js::execCmd] failed to execute command.", e)
}
}
}
}
async handleMessage(message, portOrSender, sendResponse) {
await this.execCmd(message, portOrSender, sendResponse);
if (message.forwardToSameFramePort) {
this.sendToFrameContentScripts(message, portOrSender.tab.id, portOrSender.frameId, message.port)
}
if (message.forwardToContentScript) {
this.logger.log('info', 'comms', "[CommsServer.js::processReceivedMessage] Message has 'forward to content script' flag set. Forwarding message as is. Message:", message);
this.sendToFrame(message, message.targetTab, message.targetFrame);
}
if (message.forwardToAll) {
this.logger.log('info', 'comms', "[CommsServer.js::processReceivedMessage] Message has 'forward to all' flag set. Forwarding message as is. Message:", message);
this.sendToAll(message);
}
if (message.forwardToActive) {
this.logger.log('info', 'comms', "[CommsServer.js::processReceivedMessage] Message has 'forward to active' flag set. Forwarding message as is. Message:", message);
this.sendToActive(message);
}
}
async processReceivedMessage(message, port){
this.logger.log('info', 'comms', "[CommsServer.js::processReceivedMessage] Received message from popup/content script!", message, "port", port);
this.handleMessage(message, port)
}
processReceivedMessage_nonpersistent(message, sender, sendResponse){
this.logger.log('info', 'comms', "%c[CommsServer.js::processMessage_nonpersistent] Received message from background script!", "background-color: #11D; color: #aad", message, sender);
this.handleMessage(message, sender, sendResponse);
}
// chrome shitiness mitigation
sendUnmarkPlayer(message) {
this.logger.log('info', 'comms', '[CommsServer.js::sendUnmarkPlayer] Chrome is a shit browser that doesn\'t do port.postMessage() in unload events, so we have to resort to inelegant hacks. If you see this, then the workaround method works.');
this.processReceivedMessage(message, this.popupPort);
}
}
export default CommsServer;

View File

@ -0,0 +1,183 @@
import UI from './UI';
import VuexWebExtensions from 'vuex-webextensions';
import NotificationUi from '../../../csui/NotificationUi.vue';
import Notifications from '../../../common/data/notifications';
if (process.env.CHANNEL !== 'stable'){
console.info("Loading: PlayerNotificationUi");
}
let MuteScope = Object.freeze({
CurrentSite: 'current-site',
Global: 'global'
});
class PlayerNotificationUi extends UI {
constructor (
playerElement,
settings
) {
super(
'notification',
PlayerNotificationUi.getStoreConfig(),
PlayerNotificationUi.getUiConfig(playerElement),
PlayerNotificationUi.getCommsConfig()
);
this.settings = settings;
}
//#region constructor helpers
// we will move some things out of the constructor in order to keep things clean
static getStoreConfig() {
return {
plugins: [
VuexWebExtensions({
persistentStates: [
'notificationConfig'
],
}),
],
state: {
// should be null by default!
notificationConfig: {
text: 'sample text <b>now with 100% more html formatting!</b>',
icon: 'exclamation-circle',
timeout: 5000,
}
},
mutations: {
'uw-set-notification'(state, payload) {
state['notificationConfig'] = payload;
}
},
actions: {
'uw-set-notification'({commit}, payload) {
commit('uw-set-notification', payload);
}
}
};
}
static getUiConfig(playerElement) {
return {
parentElement: playerElement,
component: NotificationUi
}
}
static getCommsConfig() {
return {
handlers: {
'show-notification': [(message) => this.showNotification(message)],
}
}
}
//#endregion
//#region lifecycle
replace(playerElement) {
super.replace(this.getUiConfig(playerElement));
}
//#endregion
/**
* Show notification on screen.
*
* @param notificationConfig notification config (or ID of notification config in /common/data/notifications.js)
*
* notificationConfig should resemble this:
* {
* timeout: number how long we'll be displaying the notification. If empty, 10s. -1: until user dismisses it
* icon: string what icon we're gonna show. We're using bootstrap icons. Can be empty.
* text: notification text. Supports HTML.
* notificationActions: [
* {
* command: function that gets executed upon clicking the button.
* label: label of the button
* icon: icon of the button
* }
* ],
* hideActions: [
* // more of notificationActions but with special case
* ]
* }
*
* When notificationConfig is a string, the function will add two additional notifications on the notificationActionsPile
* * never show this notification ever again on any site
* * never show this notification again on this site
*/
showNotification(notificationConfig) {
if (typeof notificationConfig === 'string') {
try {
const config = Notifications[notificationConfig];
// this should _never_ appear on production version of the extension, but it should help with development.
if (!config) {
return this.vuexStore?.dispatch('uw-set-notification', {
icon: 'x-circle-fill',
text: `Notification for key ${notificationConfig} does not exist.`,
timeout: -1,
});
}
// don't show notification if it's muted
if (this.isNotificationMuted(notificationConfig)) {
return;
}
this.vuexStore?.dispatch('uw-set-notification', {
...config,
hideActions: [
{
command: () => this.muteNotification(notificationConfig, MuteScope.CurrentSite),
label: 'this site'
},
{
command: () => this.muteNotification(notificationConfig, MuteScope.Global),
label: 'never ever'
}
]
});
} catch (e) {
console.error('theres been an error:', e)
}
} else {
this.vuexStore?.dispatch('uw-set-notification', notificationConfig);
}
}
muteNotification(notificationId, scope) {
// ensure objects we try to set exist
if (!this.settings.active.mutedNotifications) {
this.settings.active.mutedNotifications = {};
}
if (!this.settings.active.mutedNotifications[notificationId]) {
this.settings.active.mutedNotifications[notificationId] = {};
}
// actually mute notification
if (scope === MuteScope.Global) {
this.settings.active.mutedNotifications[notificationId].$global = true;
} else {
this.settings.active.mutedNotifications[notificationId][window.location.hostname] = true;
}
// persist settings
this.settings.saveWithoutReload();
}
isNotificationMuted(notificationId) {
return this.settings.active.mutedNotifications?.[notificationId]?.$global
|| this.settings.active.mutedNotifications?.[notificationId]?.[window.location.hostname];
}
}
if (process.env.CHANNEL !== 'stable'){
console.info("PlayerNotificationUi loaded");
}
export default PlayerNotificationUi;

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