Merge branch '4.0.1' into stable

This commit is contained in:
Tamius Han 2019-06-05 23:39:20 +02:00
commit 13d0e29e46
163 changed files with 17037 additions and 5758 deletions

26
.babelrc Normal file
View File

@ -0,0 +1,26 @@
{
"plugins": [
],
"presets": [
["@babel/preset-env", {
"useBuiltIns": false,
"targets": {
"esmodules": true,
},
}]
]
}
// {
// "plugins": [
// "@babel/plugin-proposal-optional-chaining"
// ],
// "presets": [
// ["@babel/preset-env", {
// "useBuiltIns": "usage",
// "targets": {
// // https://jamie.build/last-2-versions
// "browsers": ["> 0.25%", "not ie 11", "not op_mini all"]
// }
// }]
// ]
// }

7
.gitignore vendored
View File

@ -1,4 +1,7 @@
ultrawidify.zip
ultrawidify-git.zip
old/
build/
/node_modules
/*.log
/dist
/dist-zip
/uw-git_keys

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

17
.vscode/launch.json vendored
View File

@ -5,10 +5,19 @@
"version": "0.2.0",
"configurations": [
{
"type": "node",
"name": "Launch addon",
"type": "firefox",
"request": "launch",
"name": "Launch Program",
"program": "${file}"
"port":6000,
"reAttach": true,
"addonType": "webExtension",
"addonPath": "${workspaceFolder}/dist",
}
]
],
"firefox": {
"executable": "/usr/bin/firefox-developer-edition",
"firefoxArgs": [
"--start-debugger-server"
]
}
}

View File

@ -1,8 +1,38 @@
# Changelog
## v4.x
### Plans for the future
* Allow users to set autodetection sensitivity
* Settings page looks ugly af right now. Maybe fix it some time later
### v4.0.0 (upcoming)
* Using vue for popup and settings page
* Editable shortcuts
* Per-site controls
* You can now select which specific video on the page you control, provided each video is in its separate iframe
* Basic mode added
* Rewrote keyboard shortcuts and changed how they're handled. Massively.
* Fixed the bug where saving settings wouldn't work
* Fixed the bug where autodetection didn't calculate aspect ratio correctly. This bug would manifest in this extension cropping too much video even in cases where edge between letterbox and video was clearly defined.
* Implemented/improved black frame detection
* Autodetection now differentiates between legitimate letterbox and linear gradients. This should prevent incorrect auto-cropping on videos that look similar to IGN's [Hollow Knight](https://www.youtube.com/watch?v=hg25ONutphA) review.
## v3.x
### v3.2.2
~~### v3.3.0~~
~~This will probably get promoted to 4.0, continuing the trend of version something.3 not happening. Eulul~~
* ~~Basic mode added~~
* ~~Per-site controls in popup (to control embedded videos)~~
* ~~Rewrote keyboard shortcuts and changed how they're handled. Massively.~~
Never happened, got bumped to 4.0.0.
### v3.2.2 (current)
* Pan event listener now gets properly unbound
* Fixed 'reset zoom' button in popup

21
README-AMO.md Normal file
View File

@ -0,0 +1,21 @@
# Build guide for AMO
## Build platform
The extension is built on a PC running Manjaro Linux. Yarn is the package manager of choice.
Yarn version: 1.16.0
Node version: v11.15.0
## Installing dependencies
`yarn install --frozen-lockfile`
According to Yarn documentation, that should install the exact version of dependencies that the extension is using.
## Reproducing build
`npm run build`
The compiled code pops up in /dist.

View File

@ -1,15 +1,20 @@
# 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](#edge-speficic-limitations-important).
## TL;DR
If you own an ultrawide monitor, you have probably noticed that sometimes videos aren't encoded properly — they feature black bars on all four sides. This could happen because someone was incompetent (note: as far as youtube is concerned, improperly rendered videos might be due to youtube's implementation of certain new features). The extension kinda fixes that by doing this:
![Demo](img-demo/example-httyd2.png "Should these black bars be here? No [...] But an ultrawide user never forgets.")
## Known issues
* Netflix autodetection not working in Chrome, wontfix as issue is fundamentally unfixable.
* Keyboard shortcut for automatic detection (A) doesn't really work for some reason.
* Everything reported in [issues](https://github.com/xternal7/ultrawidify/issues)
### Limitations
@ -17,7 +22,8 @@ If you own an ultrawide monitor, you have probably noticed that sometimes videos
* Unclear how extension handles sites with more than one video per page.
* Autodetection is only correct 95% of the time, most of the time.
* That new stretching mode wasn't thoroughly tested yet. Issues may be present. (Same with zoom)
* Enabling extension everywhere (as opposed to whitelisted sites) could break some websites.
* Enabling extension everywhere (as opposed to whitelisted sites) could break some websites.
* Edge has
### Features
@ -45,9 +51,15 @@ 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)
### Beggathon
This extension also has a version for Microsoft Edge, but that requires a bit more effort. [See notes first](#edge-speficic-limitations-important).
Working on this extension takes time, coffee and motivation. If you want to buy me a beer or something, you can [use this link to send me motivation](https://www.paypal.me/tamius). **Any donations are well appreciated.**
# Beggathon (donations)
If you want to support this project, please consider a donation. Working on this extension takes time, money, coffee and motivation. Sometimes also [a very precise amount of alco](https://xkcd.com/323/).
You can make a donation [via Paypal](https://www.paypal.me/tamius).
**Any donation — no matter how big or small — is well appreciated. Thanks.**
 
@ -211,6 +223,8 @@ is currently not possible. Settings page for this extension has been disabled so
However, I do plan on implementing this feature. Hopefully by the end of the year, but given how consistently I've been breaking self-imposed deadlines and goals for this extension don't hold your breath. After all, [Hofstadter's a bitch](https://en.wikipedia.org/wiki/Hofstadter%27s_law).
## Plans for the future
1. Handle porting of extension settings between versions.
@ -220,24 +234,54 @@ However, I do plan on implementing this feature. Hopefully by the end of the yea
5. figure the best way to do GUI (injecting buttons into the player bar is not a good way. Been there, done that, each site has its own way and some appear to be impossible). ~~Might get bumped to be released alongside #2~~no it wont lol
6. Improvements to automatic aspect ratio detection
## Installing
### Permanent install / stable
[Latest stable for Firefox — download from AMO](https://addons.mozilla.org/en/firefox/addon/ultrawidify/)
[Latest stafle for Chrome — download from Chrome store](https://chrome.google.com/webstore/detail/ultrawidify/dndehlekllfkaijdlokmmicgnlanfjbi)
[Latest stable for Chrome — download from Chrome store](https://chrome.google.com/webstore/detail/ultrawidify/dndehlekllfkaijdlokmmicgnlanfjbi)
### Installing the current, github version
Requirements: npm, yarn.
1. Clone this repo
2. run `yarn install`
3. run `npm run watch:dev`
TODO: see if #3 already loads the extension in FF
2. Open up Firefox
3. Go to `about:debugging`
4. Add temporary addon
5. Browse to wherever you saved it and select manifest.json
5. Select `${ultrawidify_folder}/dist/manifest.json`
## Changelog
# Edge-specific limitations (IMPORTANT!)
For various reasons — most notably, I refuse to pay Microsoft €14 for the privilege of developing shit for their outright broken browser (and in addition to that, the extension needs to go through review process as well) — this extension isn't going to appear on Microsoft Store. (And I do not permit anyone else to do that in my name either). Full rant on why I've made this decision can be found [here](https://github.com/xternal7/ultrawidify/issues/14#issuecomment-424903335).
As a result, you'll have to download the extension and install it manually. This approach has some downsides.
1. You'll get this popup after starting Edge. If you've already opened Youtube or Netflix, **you will have to reload the page (or navigate to somewhere else) in order for extension to start.**
![slika](https://user-images.githubusercontent.com/12505802/46114175-05912200-c1e1-11e8-91c7-2217f5bf79e3.png)
2. Certain WebExtension APIs that I rely on are outright broken in Edge. This bug would cause global extension settings (tab: Extension settings) and per-site settings (tab: Site settings) to reset to default values every time you'd open the popup. As a result, _Extension settings_ and _Site settings_ tabs are disabled in Edge:
![Feast on dem popup](https://user-images.githubusercontent.com/12505802/46113923-d1693180-c1df-11e8-82f0-ad64cbc57558.png)
Unfortunate consequence of this is that you won't be able to enable this extension for sites other than Youtube and Netflix, but then again. Let's be honest. You're only using Edge for Netflix, so that's probably no big deal for you.
**It's also worth noting that I'm not actively maintaining the Edge fork, so it's a few versions behind.**
## Installing Ultrawidify in M$ Edge
1. Download the zip file from [here](https://github.com/xternal7/ultrawidify/tree/master/releases/edge)
2. Extract contents of the zip file in some folder. It really doesn't matter where, just keep it somewhere that won't be in your way when using your computer.
3. Follow the steps in [this guide](https://docs.microsoft.com/en-us/microsoft-edge/extensions/guides/adding-and-removing-extensions)
# Changelog
see changelog.md
todo: add link to changelog.md here
todo: add link to changelog.md here

View File

@ -1,49 +0,0 @@
// Set prod to true when releasing
_prod = true;
// _prod = false;
Debug = {
init: true,
debug: false,
keyboard: true,
debugResizer: true,
debugArDetect: true,
debugStorage: false,
comms: false,
// showArDetectCanvas: true,
flushStoredSettings: true,
// flushStoredSettings: false,
playerDetectDebug: true,
periodic: true,
videoRescan: true,
arDetect: {
edgeDetect: true
},
canvas: {
debugDetection: true
},
debugCanvas: {
// enabled: true,
// guardLine: true
enabled: false,
guardLine: false
}
}
if(_prod){
__disableAllDebug(Debug);
}
function __disableAllDebug(obj) {
for(key in obj) {
if (obj.hasOwnProperty(key) ){
if(obj[key] instanceof Object)
__disableAllDebug(obj[key]);
else
obj[key] = false;
}
}
}
if(Debug.debug)
console.log("Guess we're debugging ultrawidify then. Debug.js must always load first, and others must follow.\nLoading: Debug.js");

View File

@ -1,246 +0,0 @@
if(Debug.debug)
console.log("Loading: ExtensionConf.js");
var ExtensionConf = {
extensionMode: "whitelist", // how should this extension work?
// 'blacklist' - work everywhere except blacklist
// 'whitelist' - only work on whitelisted sites
// 'disabled' - work nowhere
arDetect: {
mode: "blacklist", // how should autodetection work?
// 'blacklist' - work by default, problem sites need to be blocked
// 'whitelist' - only work if site has been specifically approved
// 'disabled' - don't work at all
disabledReason: "", // if automatic aspect ratio has been disabled, show reason
allowedMisaligned: 0.05, // top and bottom letterbox thickness can differ by this much.
// Any more and we don't adjust ar.
allowedArVariance: 0.075, // amount by which old ar can differ from the new (1 = 100%)
timer_playing: 666, // we trigger ar this often (in ms) under this conditions
timer_paused: 3000,
timer_error: 3000,
timer_minimumTimeout: 5, // but regardless of above, we wait this many msec before retriggering
autoDisable: { // settings for automatically disabling the extension
maxExecutionTime: 6000, // if execution time of main autodetect loop exceeds this many milliseconds,
// we disable it.
consecutiveTimeoutCount: 5, // we only do it if it happens this many consecutive times
// FOR FUTURE USE
consecutiveArResets: 5 // if aspect ratio reverts immediately after AR change is applied, we disable everything
},
hSamples: 640,
vSamples: 360,
// samplingInterval: 10, // we sample at columns at (width/this) * [ 1 .. this - 1]
blackLevel_default: 10, // everything darker than 10/255 across all RGB components is considered black by
// default. blackLevel can decrease if we detect darker black.
blackbarTreshold: 16, // if pixel is darker than blackLevel + blackbarTreshold, we count it as black
// on 0-255. Needs to be fairly high (8 might not cut it) due to compression
// artifacts in the video itself
variableBlackbarTresholdOptions: { // In case of poor bitrate videos, jpeg artifacts may cause us issues
// FOR FUTURE USE
enabled: true, // allow increasing blackbar threshold
disableArDetectOnMax: true, // disable autodetection when treshold goes over max blackbar treshold
maxBlackbarTreshold: 48, // max threshold (don't increase past this)
thresholdStep: 8, // when failing to set aspect ratio, increase treshold by this much
increaseAfterConsecutiveResets: 2 // increase if AR resets this many times in a row
},
staticSampleCols: 9, // we take a column at [0-n]/n-th parts along the width and sample it
randomSampleCols: 0, // we add this many randomly selected columns to the static columns
staticSampleRows: 9, // forms grid with staticSampleCols. Determined in the same way. For black frame checks
guardLine: { // all pixels on the guardline need to be black, or else we trigger AR recalculation
// (if AR fails to be recalculated, we reset AR)
enabled: true,
ignoreEdgeMargin: 0.20, // we ignore anything that pokes over the black line this close to the edge
// (relative to width of the sample)
imageTestTreshold: 0.1, // when testing for image, this much pixels must be over blackbarTreshold
edgeTolerancePx: 2, // black edge violation is performed this far from reported 'last black pixel'
edgeTolerancePercent: null // unused. same as above, except use % of canvas height instead of pixels
},
fallbackMode: {
enabled: true,
safetyBorderPx: 5, // determines the thickness of safety border in fallback mode
noTriggerZonePx: 8 // if we detect edge less than this many pixels thick, we don't correct.
},
arSwitchLimiter: { // to be implemented
switches: 2, // we can switch this many times
period: 2.0 // per this period
},
edgeDetection: {
sampleWidth: 8, // we take a sample this wide for edge detection
detectionTreshold: 4, // sample needs to have this many non-black pixels to be a valid edge
singleSideConfirmationTreshold: 0.3, // 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)
logoTreshold: 0.15, // if edge candidate sits with count greater than this*all_samples, it can't be logo
// or watermark.
edgeTolerancePx: 2, // we check for black edge violation this far from detection point
edgeTolerancePercent: null, // we check for black edge detection this % of height from detection point. unused
middleIgnoredArea: 0.2, // we ignore this % of canvas height towards edges while detecting aspect ratios
minColsForSearch: 0.5, // 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)
edgeTolerancePx: 1, // tests for edge detection are performed this far away from detected row
},
pillarTest: {
ignoreThinPillarsPx: 5, // ignore pillars that are less than this many pixels thick.
allowMisaligned: 0.05 // left and right edge can vary this much (%)
},
textLineTest: {
nonTextPulse: 0.10, // 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: 10, // this is a treshold to confirm we're seeing text.
pulsesToConfirmIfHalfBlack: 5, // this is the treshold to confirm we're seeing text if longest black pulse
// is over 50% of the canvas width
testRowOffset: 0.02 // we test this % of height from detected edge
}
},
arChange: {
samenessTreshold: 0.025, // if aspect ratios are within 2.5% within each other, don't resize
},
zoom: {
minLogZoom: -1,
maxLogZoom: 3,
announceDebounce: 200 // we wait this long before announcing new zoom
},
miscFullscreenSettings: {
videoFloat: "center",
mousePan: {
enabled: false
},
defaultAr: "original",
},
stretch: {
initialMode: 0, // 0 - no stretch, 1 - basic, 2 - hybrid, 3 - conditional
conditionalDifferencePercent: 0.05 // black bars less than this wide will trigger stretch
// if mode is set to '1'. 1.0=100%
},
resizer: {
setStyleString: {
maxRetries: 3,
retryTimeout: 200
}
},
pageInfo: {
timeouts: {
urlCheck: 200,
rescan: 1500
}
},
colors:{
// criticalFail: "background: #fa2; color: #000"
},
keyboard: {
shortcuts: {
// automatic
"a": {
action: "auto-ar",
},
//#region crop
"r": {
action: "crop",
arg: "reset"
},
"w": {
action: "crop",
arg: "fitw"
},
"e": {
action: "crop",
arg: "fith"
},
"s": {
action: "crop",
arg: 1.78
},
"d": {
action: "crop",
arg: 2.39
},
"x": {
action: "crop",
arg: 2.0
},
"q": {
action: "crop",
arg: 2.35
},
//#endregion
//#region zoom
"z": {
action: "zoom",
arg: 0.1
},
"u": {
action: "zoom",
arg: -0.1
},
"p": {
action: "pan",
arg: 'toggle' // possible: 'enable', 'disable', 'toggle'
},
"shiftKey_shift": {
action: "pan",
arg: 'toggle',
keyup: {
action: 'pan',
arg: 'toggle'
}
},
"shift": {
action: "",
arg: "",
keyup: {
action: 'pan',
arg: 'toggle',
}
}
//#endregion
},
modKeys: ["ctrlKey", "shiftKey", "altKey"]
},
// -----------------------------------------
// ::: SITE CONFIGURATION :::
// -----------------------------------------
// 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
//
// defaultAar?: <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
// }
//
// Veljavne vrednosti za možnosti
// Valid values for options:
//
// status, arStatus, statusEmbedded:
//
// * enabled — always allow
// * default — allow if default is to allow, block if default is to block
// * disabled — never allow
//
sites: {
"www.youtube.com" : {
status: "enabled", // should extension work on this site?
arStatus: "default", // should we enable autodetection
statusEmbedded: "enabled", // should extension work for this site when embedded on other sites?
override: false, // ignore value localStorage in favour of this
type: 'official' // is officially supported? (Alternatives are 'community' and 'user-defined')
},
"www.netflix.com" : {
status: "enabled",
arStatus: BrowserDetect.firefox ? "default" : "disabled",
statusEmbedded: "enabled",
override: false,
type: 'official'
},
}
}

View File

@ -1,85 +0,0 @@
if(Debug.debug)
console.log("Loading: Keybinds.js");
class Keybinds {
constructor(pageInfo){
this.pageInfo = pageInfo;
this.settings = pageInfo.settings;
this.inputs = ['input','select','button','textarea'];
}
setup(){
var ths = this;
document.addEventListener('keydown', (event) => ths.handleKeypress(event) );
document.addEventListener('keyup', (event) => ths.handleKeypress(event,true) );
}
handleKeypress(event, isKeyUp) { // Tukaj ugotovimo, katero tipko smo pritisnili
if(Debug.debug && Debug.keyboard ){
console.log("%c[Keybinds::_kbd_process] we pressed a key: ", "color: #ff0", event.key , " | keydown: ", event.keydown, "event:", event);
}
// Tipke upoštevamo samo, če smo v celozaslonskem načinu oz. če ne pišemo komentarja
// v nasprotnem primeru ne naredimo nič.
// We only take actions if we're in full screen or not writing a comment
var activeElement = document.activeElement;
if( (! PlayerData.isFullScreen()) && (
(this.inputs.indexOf(activeElement.tagName.toLocaleLowerCase()) !== -1) ||
(activeElement.getAttribute("role") === "textbox") ||
(activeElement.getAttribute("type") === "text")
)){
if(Debug.debug && Debug.keyboard)
console.log("[Keybinds::_kbd_process] We're writing a comment or something. Doing nothing");
return;
}
// building modifiers list:
var modlist = "";
for(var mod of this.settings.active.keyboard.modKeys){
if(event[mod])
modlist += (mod + "_")
}
if(Debug.debug && Debug.keyboard ){
if(modlist)
console.log("[Keybinds::_kbd_process] there's been modifier keys. Modlist:", modlist);
}
var keypress = modlist + event.key.toLowerCase();
if(Debug.debug && Debug.keyboard )
console.log("[Keybinds::_kbd_process] our full keypress is this", keypress );
if(this.settings.active.keyboard.shortcuts[keypress]){
var conf = this.settings.active.keyboard.shortcuts[keypress];
if (isKeyUp) {
if (conf.keyup) {
conf = conf.keyup;
} else {
return;
}
}
if(Debug.debug && Debug.keyboard) {
console.log("[Keybinds::_kbd_process] there's an action associated with this keypress. conf:", conf, "conf.arg:", conf.arg);
}
if (conf.action === "crop"){
this.pageInfo.stopArDetection();
this.pageInfo.setAr(conf.arg);
} else if (conf.action === "zoom"){
this.pageInfo.stopArDetection();
this.pageInfo.zoomStep(conf.arg);
} else if (conf.action === "auto-ar"){
this.pageInfo.startArDetection();
} else if (conf.action === "pan") {
this.pageInfo.setPanMode(conf.arg);
}
}
}
}

View File

@ -1,48 +0,0 @@
var _bd_usebrowser = "firefox";
var _bd_isFirefox = true;
var _bd_isChrome = false;
var _bd_isEdge = false; // we'll see if FF
try{
// todo: find something that works in firefox but not in edge (or vice-versa)
// note that this function returns a promise! and is broken for some reason
var browserinfo = browser.runtime.getBrowserInfo();
// we don't need to actually check because only firefox supports that.
// if we're not on firefox, the above call will probably throw an exception anyway.
// if browsers other than firefox start supporting that, well ... we'll also need to actually await for promise
// that getBrowserInfo() returns to resolve.
// if (Browser.name.toLowerCase().indexOf(firefox) !== -1 || Browser.vendor.toLowerCase().indexOf(mozilla) !== -1) {
_bd_isFirefox = true;
_bd_isEdge = false;
// }
}
catch (e) {
if(Debug.debug) {
console.info("[BrowserDetect] browser.runtime.getBrowserInfo() probably failed. This means we're probably not using firefox.", e)
}
};
if(typeof browser === "undefined"){ // This is a good sign we're in chrome or chromium-based browsers
if(chrome){
browser = chrome;
_bd_usebrowser = "chrome";
_bd_isChrome = true;
_bd_isEdge = false;
_bd_isFirefox = false;
}
}
var BrowserDetect = {
usebrowser: _bd_usebrowser,
firefox: _bd_isFirefox,
chrome: _bd_isChrome,
edge: _bd_isEdge
}
if(Debug.debug){
console.log("BrowserDetect loaded! Here's BrowserDetect object:", BrowserDetect)
}

View File

@ -1,409 +0,0 @@
if(Debug.debug){
console.log("Loading Comms.js");
}
class CommsClient {
constructor(name, settings) {
if (BrowserDetect.firefox) {
this.port = browser.runtime.connect({name: name});
} else if (BrowserDetect.chrome) {
this.port = chrome.runtime.connect({name: name});
} else if (BrowserDetect.edge) {
this.port = browser.runtime.connect({name: name})
}
var ths = this;
this._listener = m => ths.processReceivedMessage(m);
this.port.onMessage.addListener(this._listener);
this.settings = settings;
this.pageInfo = undefined;
this.commsId = (Math.random() * 20).toFixed(0);
}
destroy() {
this.pageInfo = null;
this.settings = null;
this.port.onMessage.removeListener(this._listener);
}
setPageInfo(pageInfo){
this.pageInfo = pageInfo;
if(Debug.debug) {
console.log(`[CommsClient::setPageInfo] <${this.commsId}>`, "SETTING PAGEINFO —", this.pageInfo, this)
}
var ths = this;
this._listener = m => ths.processReceivedMessage(m);
this.port.onMessage.removeListener(this._listener);
this.port.onMessage.addListener(this._listener);
}
processReceivedMessage(message){
if(Debug.debug && Debug.comms){
console.log(`[CommsClient.js::processMessage] <${this.commsId}> Received message from background script!`, message);
}
if (!this.pageInfo || !this.settings.active) {
if(Debug.debug && Debug.comms){
console.log(`[CommsClient.js::processMessage] <${this.commsId}> this.pageInfo (or settings) not defined. Extension is probably disabled for this site.\npageInfo:`, this.pageInfo,
"\nsettings.active:", this.settings.active,
"\nnobj:", this
);
}
return;
}
if (message.cmd === 'get-current-zoom') {
this.pageInfo.requestCurrentZoom();
}
if (message.cmd === "set-ar") {
this.pageInfo.setAr(message.ratio);
} else if (message.cmd === 'set-alignment') {
this.pageInfo.setVideoFloat(message.mode);
this.pageInfo.restoreAr();
} else if (message.cmd === "set-stretch") {
this.pageInfo.setStretchMode(StretchMode[message.mode]);
} else if (message.cmd === "autoar-start") {
if (message.enabled !== false) {
this.pageInfo.initArDetection();
this.pageInfo.startArDetection();
} else {
this.pageInfo.stopArDetection();
}
} else if (message.cmd === "pause-processing") {
this.pageInfo.pauseProcessing();
} else if (message.cmd === "resume-processing") {
// todo: autoArStatus
this.pageInfo.resumeProcessing(message.autoArStatus);
} else if (message.cmd === 'set-zoom') {
this.pageInfo.setZoom(message.zoom, true);
}
}
async sleep(n){
return new Promise( (resolve, reject) => setTimeout(resolve, n) );
}
async sendMessage_nonpersistent(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);
});
}
}
catch(e){
reject(e);
}
});
}
}
async requestSettings(){
if(Debug.debug){
console.log("%c[CommsClient::requestSettings] sending request for congif!", "background: #11D; color: #aad");
}
var response = await this.sendMessage_nonpersistent({cmd: 'get-config'});
if(Debug.debug){
console.log("%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);
}
registerVideo(){
this.port.postMessage({cmd: "has-video"});
}
announceZoom(scale){
this.port.postMessage({cmd: "announce-zoom", zoom: scale});
}
unregisterVideo(){
this.port.postMessage({cmd: "noVideo"}); // ayymd
}
}
class CommsServer {
constructor(server) {
this.server = server;
this.settings = server.settings;
this.ports = [];
var ths = this;
if (BrowserDetect.firefox) {
browser.runtime.onConnect.addListener(p => ths.onConnect(p));
browser.runtime.onMessage.addListener(m => ths.processReceivedMessage_nonpersistent_ff(m));
} else {
chrome.runtime.onConnect.addListener(p => ths.onConnect(p));
chrome.runtime.onMessage.addListener((msg, sender, callback) => ths.processReceivedMessage_nonpersistent_chrome(m, sender, callback));
}
}
async getCurrentTabHostname() {
const activeTab = await this._getActiveTab();
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(var p of this.ports){
for(var frame in p){
p[frame].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);
});
});
}
}
async sendToActive(message) {
if(Debug.debug && Debug.comms){
console.log("%c[CommsServer::sendToActive] trying to send a message to active tab. Message:", "background: #dda; color: #11D", message);
}
var tabs = await this._getActiveTab();
if(Debug.debug && Debug.comms){
console.log("[CommsServer::_sendToActive_ff] currently active tab(s)?", tabs);
for (var key in this.ports[tabs[0].id]) {
console.log("key?", key, this.ports[tabs[0].id]);
// this.ports[tabs[0].id][key].postMessage(message);
}
}
for (var key in this.ports[tabs[0].id]) {
this.ports[tabs[0].id][key].postMessage(message);
}
}
onConnect(port){
var ths = this;
// poseben primer | special case
if (port.name === 'popup-port') {
this.popupPort = port;
this.popupPort.onMessage.addListener( (m,p) => ths.processReceivedMessage(m,p));
return;
}
var tabId = port.sender.tab.id;
var frameId = port.sender.frameId;
if(! this.ports[tabId]){
this.ports[tabId] = {};
}
this.ports[tabId][frameId] = port;
this.ports[tabId][frameId].onMessage.addListener( (m,p) => ths.processReceivedMessage(m, p));
this.ports[tabId][frameId].onDisconnect.addListener( (p) => {
delete ths.ports[p.sender.tab.id][p.sender.frameId];
if(Object.keys(ths.ports[p.sender.tab.id]).length === 0){
ths.ports[tabId] = undefined;
}
});
}
async processReceivedMessage(message, port){
if (Debug.debug && Debug.comms) {
console.log("[CommsServer.js::processMessage] Received message from background script!", message, "port", port, "\nsettings and server:", this.settings,this.server);
}
if (message.cmd === 'announce-zoom') {
// forward off to the popup, no use for this here
try {
this.popupPort.postMessage({cmd: 'set-current-zoom', zoom: message.zoom});
} catch (e) {
// can't forward stuff to popup if it isn't open
}
} else if (message.cmd === 'get-current-zoom') {
this.sendToActive(message);
}
if (message.cmd === 'get-current-site') {
port.postMessage({cmd: 'set-current-site', site: await this.getCurrentTabHostname()});
}
if (message.cmd === 'get-config') {
if(Debug.debug) {
console.log("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})
} else if (message.cmd === 'set-stretch') {
this.sendToActive(message);
} else if (message.cmd === 'set-ar') {
this.sendToActive(message);
} else if (message.cmd === 'set-custom-ar') {
this.settings.active.keyboard.shortcuts.q.arg = message.ratio;
this.settings.save();
} else if (message.cmd === 'set-alignment') {
this.sendToActive(message);
} else if (message.cmd === 'autoar-start') {
this.sendToActive(message);
} else if (message.cmd === "autoar-disable") { // LEGACY - can be removed prolly
this.settings.active.arDetect.mode = "disabled";
if(message.reason){
this.settings.active.arDetect.disabledReason = message.reason;
} else {
this.settings.active.arDetect.disabledReason = 'User disabled';
}
this.settings.save();
} else if (message.cmd === 'set-zoom') {
this.sendToActive(message);
}
}
processReceivedMessage_nonpersistent_ff(message, sender){
if (Debug.debug && Debug.comms) {
console.log("%c[CommsServer.js::processMessage_nonpersistent_ff] Received message from background script!", "background-color: #11D; color: #aad", message, sender);
}
if (message.cmd === 'get-config') {
var ret = {extensionConf: JSON.stringify(this.settings.active)};
if (Debug.debug && Debug.comms) {
console.log("%c[CommsServer.js::processMessage_nonpersistent_ff] Returning this:", "background-color: #11D; color: #aad", ret);
}
Promise.resolve(ret);
} else if (message.cmd === "autoar-enable") {
this.settings.active.arDetect.mode = "blacklist";
this.settings.save();
this.sendToAll({cmd: "reload-settings", sender: "uwbg"})
if(Debug.debug){
console.log("[uw-bg] autoar set to enabled (blacklist). evidenz:", this.settings.active);
}
} else if (message.cmd === "autoar-disable") {
this.settings.active.arDetect.mode = "disabled";
if(message.reason){
this.settings.active.arDetect.disabledReason = message.reason;
} else {
this.settings.active.arDetect.disabledReason = 'User disabled';
}
this.settings.save();
this.sendToAll({cmd: 'reload-settings', newConf: this.settings.active});
if(Debug.debug){
console.log("[uw-bg] autoar set to disabled. evidenz:", this.settings.active);
}
} else if (message.cmd === "autoar-set-interval") {
if(Debug.debug)
console.log("[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();
this.sendToAll({cmd: 'reload-settings', newConf: this.settings.active});
}
}
processReceivedMessage_nonpersistent_chrome(message, sender, sendResponse){
if (Debug.debug && Debug.comms) {
console.log("[CommsServer.js::processMessage_nonpersistent_chrome] Received message from background script!", message);
}
if(message.cmd === 'get-config') {
sendResponse({extensionConf: JSON.stringify(this.settings.active), site: this.getCurrentTabHostname()});
// return true;
} else if (message.cmd === "autoar-enable") {
this.settings.active.arDetect.mode = "blacklist";
this.settings.save();
this.sendToAll({cmd: "reload-settings", sender: "uwbg"})
if(Debug.debug){
console.log("[uw-bg] autoar set to enabled (blacklist). evidenz:", this.settings.active);
}
} else if (message.cmd === "autoar-disable") {
this.settings.active.arDetect.mode = "disabled";
if(message.reason){
this.settings.active.arDetect.disabledReason = message.reason;
} else {
this.settings.active.arDetect.disabledReason = 'User disabled';
}
this.settings.save();
this.sendToAll({cmd: 'reload-settings', newConf: this.settings.active});
if(Debug.debug){
console.log("[uw-bg] autoar set to disabled. evidenz:", this.settings.active);
}
} else if (message.cmd === "autoar-set-interval") {
if(Debug.debug)
console.log("[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();
this.sendToAll({cmd: 'reload-settings', newConf: this.settings.active});
}
}
}
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);
});
}
}
catch(e){
reject(e);
}
});
}
}
}

View File

@ -1,649 +0,0 @@
var EdgeDetectPrimaryDirection = {
VERTICAL: 0,
HORIZONTAL: 1
}
var EdgeDetectQuality = {
FAST: 0,
IMPROVED: 1
}
class EdgeDetect{
constructor(ardConf){
this.conf = ardConf;
this.settings = ardConf.settings;
this.sampleWidthBase = this.settings.active.arDetect.edgeDetection.sampleWidth << 2; // corrected so we can work on imageData
this.halfSample = this.sampleWidthBase >> 1;
this.detectionTreshold = this.settings.active.arDetect.edgeDetection.detectionTreshold;
this.init(); // initiate things that can change
}
// initiates things that we may have to change later down the line
init() {
}
findBars(image, sampleCols, direction = EdgeDetectPrimaryDirection.VERTICAL, quality = EdgeDetectQuality.IMPROVED, guardLineOut){
var fastCandidates, edgeCandidates, bars;
if (direction == EdgeDetectPrimaryDirection.VERTICAL) {
fastCandidates = this.findCandidates(image, sampleCols, guardLineOut);
// if(quality == EdgeDetectQuality.FAST){
// edges = fastCandidates; // todo: processing
// } else {
edgeCandidates = this.edgeDetect(image, fastCandidates);
bars = this.edgePostprocess(edgeCandidates, this.conf.canvas.height);
// }
} else {
bars = this.pillarTest(image) ? {status: 'ar_known'} : {status: 'ar_unknown'};
}
return bars;
}
findCandidates(image, sampleCols, guardLineOut){
var upper_top, upper_bottom, lower_top, lower_bottom;
var blackbarTreshold;
var cols_a = sampleCols.slice(0);
var cols_b = cols_a.slice(0);
// todo: cloning can be done better. check array.splice or whatever
for(var i in sampleCols){
cols_b[i] = cols_a[i] + 0;
}
var res_top = [];
var res_bottom = [];
this.colsTreshold = sampleCols.length * this.settings.active.arDetect.edgeDetection.minColsForSearch;
if(this.colsTreshold == 0)
this.colsTreshold = 1;
this.blackbarTreshold = this.conf.blackLevel + this.settings.active.arDetect.blackbarTreshold;
// if guardline didn't fail and imageDetect did, we don't have to check the upper few pixels
// but only if upper and lower edge are defined. If they're not, we need to check full height
if(guardLineOut){
if(guardLineOut.imageFail && !guardLineOut.blackbarFail && this.conf.guardLine.blackbar.top) {
upper_top = this.conf.guardLine.blackbar.top;
upper_bottom = this.conf.canvas.height >> 1;
lower_top = upper_bottom;
lower_bottom = this.conf.guardLine.blackbar.bottom;
} else if (! guardLineOut.imageFail && !guardLineOut.blackbarFail && this.conf.guardLine.blackbar.top) {
// ta primer se lahko zgodi tudi zaradi kakšnega logotipa. Ker nočemo, da nam en jeben
// logotip vsili reset razmerja stranic, se naredimo hrvata in vzamemo nekaj varnostnega
// pasu preko točke, ki jo označuje guardLine.blackbar. Recimo 1/8 višine platna na vsaki strani.
// a logo could falsely trigger this case, so we need to add some extra margins past
// the point marked by guardLine.blackbar. Let's say 1/8 of canvas height on either side.
upper_top = 0;
upper_bottom = this.conf.guardLine.blackbar.top + (this.conf.canvas.height >> 3);
lower_top = this.conf.guardLine.blackbar.bottom - (this.conf.canvas.height >> 3);
lower_bottom = this.conf.canvas.height - 1;
} else {
upper_top = 0;
upper_bottom = (this.conf.canvas.height >> 1) /*- parseInt(this.conf.canvas.height * this.settings.active.arDetect.edgeDetection.middleIgnoredArea);*/
lower_top = (this.conf.canvas.height >> 1) /*+ parseInt(this.conf.canvas.height * this.settings.active.arDetect.edgeDetection.middleIgnoredArea);*/
lower_bottom = this.conf.canvas.height - 1;
}
} else{
upper_top = 0;
upper_bottom = (this.conf.canvas.height >> 1) /*- parseInt(this.conf.canvas.height * this.settings.active.arDetect.edgeDetection.middleIgnoredArea);*/
lower_top = (this.conf.canvas.height >> 1) /*+ parseInt(this.conf.canvas.height * this.settings.active.arDetect.edgeDetection.middleIgnoredArea);*/
lower_bottom = this.conf.canvas.height - 1;
}
if(Debug.debug){
console.log("[EdgeDetect::findCandidates] searching for candidates on ranges", upper_top, "<->", upper_bottom, ";", lower_top, "<->", lower_bottom)
}
var upper_top_corrected = upper_top * this.conf.canvasImageDataRowLength;
var upper_bottom_corrected = upper_bottom * this.conf.canvasImageDataRowLength;
var lower_top_corrected = lower_top * this.conf.canvasImageDataRowLength;
var lower_bottom_corrected = lower_bottom * this.conf.canvasImageDataRowLength;
if(Debug.debugCanvas.enabled){
this._columnTest_dbgc(image, upper_top_corrected, upper_bottom_corrected, cols_a, res_top, false);
this._columnTest_dbgc(image, lower_top_corrected, lower_bottom_corrected, cols_b, res_bottom, true);
} else {
this._columnTest(image, upper_top_corrected, upper_bottom_corrected, cols_a, res_top, false);
this._columnTest(image, lower_top_corrected, lower_bottom_corrected, cols_b, res_bottom, true);
}
return {res_top: res_top, res_bottom: res_bottom};
}
// dont call the following outside of this class
edgeDetect(image, samples){
var edgeCandidatesTop = {count: 0};
var edgeCandidatesBottom = {count: 0};
var detections;
var canvasWidth = this.conf.canvas.width;
var canvasHeight = this.conf.canvas.height;
var sampleStart, sampleEnd, loopEnd;
var sampleRow_black, sampleRow_color;
var blackEdgeViolation = false;
var topEdgeCount = 0;
var bottomEdgeCount = 0;
var sample;
for(sample of samples.res_top){
blackEdgeViolation = false; // reset this
// determine our bounds. Note that sample.col is _not_ corrected for imageData, but halfSample is
sampleStart = (sample.col << 2) - this.halfSample;
if(sampleStart < 0)
sampleStart = 0;
sampleEnd = sampleStart + this.sampleWidthBase;
if(sampleEnd > this.conf.canvasImageDataRowLength)
sampleEnd = this.conf.canvasImageDataRowLength;
// calculate row offsets for imageData array
sampleRow_black = (sample.top - this.settings.active.arDetect.edgeDetection.edgeTolerancePx) * this.conf.canvasImageDataRowLength;
sampleRow_color = (sample.top + 1 + this.settings.active.arDetect.edgeDetection.edgeTolerancePx) * this.conf.canvasImageDataRowLength;
// že ena kršitev črnega roba pomeni, da kandidat ni primeren
// even a single black edge violation means the candidate is not an edge
loopEnd = sampleRow_black + sampleEnd;
if(Debug.debugCanvas.enabled){
blackEdgeViolation = this._blackbarTest_dbg(image, sampleRow_black + sampleStart, loopEnd);
} else {
blackEdgeViolation = this._blackbarTest(image, sampleRow_black + sampleStart, loopEnd);
}
// če je bila črna črta skrunjena, preverimo naslednjega kandidata
// if we failed, we continue our search with the next candidate
if(blackEdgeViolation)
continue;
detections = 0;
loopEnd = sampleRow_color + sampleEnd;
if(Debug.debugCanvas.enabled) {
this._imageTest_dbg(image, sampleRow_color + sampleStart, loopEnd, sample.top, edgeCandidatesTop)
} else {
this._imageTest(image, sampleRow_color + sampleStart, loopEnd, sample.top, edgeCandidatesTop)
}
}
for(sample of samples.res_bottom){
blackEdgeViolation = false; // reset this
// determine our bounds. Note that sample.col is _not_ corrected for imageData, but this.halfSample is
sampleStart = (sample.col << 2) - this.halfSample;
if(sampleStart < 0)
sampleStart = 0;
sampleEnd = sampleStart + this.sampleWidthBase;
if(sampleEnd > this.conf.canvasImageDataRowLength)
sampleEnd = this.conf.canvasImageDataRowLength;
// calculate row offsets for imageData array
sampleRow_black = (sample.bottom + this.settings.active.arDetect.edgeDetection.edgeTolerancePx) * this.conf.canvasImageDataRowLength;
sampleRow_color = (sample.bottom - 1 - this.settings.active.arDetect.edgeDetection.edgeTolerancePx) * this.conf.canvasImageDataRowLength;
// že ena kršitev črnega roba pomeni, da kandidat ni primeren
// even a single black edge violation means the candidate is not an edge
loopEnd = sampleRow_black + sampleEnd;
if(Debug.debugCanvas.enabled){
blackEdgeViolation = this._blackbarTest_dbg(image, sampleRow_black + sampleStart, loopEnd);
} else {
blackEdgeViolation = this._blackbarTest(image, sampleRow_black + sampleStart, loopEnd);
}
// če je bila črna črta skrunjena, preverimo naslednjega kandidata
// if we failed, we continue our search with the next candidate
if(blackEdgeViolation)
continue;
detections = 0;
loopEnd = sampleRow_color + sampleEnd;
if(Debug.debugCanvas.enabled) {
this._imageTest_dbg(image, sampleRow_color + sampleStart, loopEnd, sample.bottom, edgeCandidatesBottom);
} else {
this._imageTest(image, sampleRow_color + sampleStart, loopEnd, sample.bottom, edgeCandidatesBottom);
}
}
return {
edgeCandidatesTop: edgeCandidatesTop,
edgeCandidatesTopCount: edgeCandidatesTop.count,
edgeCandidatesBottom: edgeCandidatesBottom,
edgeCandidatesBottomCount: edgeCandidatesBottom.count
};
}
edgePostprocess(edges){
var edgesTop = [];
var edgesBottom = [];
var alignMargin = this.conf.canvas.height * this.settings.active.arDetect.allowedMisaligned;
var missingEdge = edges.edgeCandidatesTopCount == 0 || edges.edgeCandidatesBottomCount == 0;
// pretvorimo objekt v tabelo
// convert objects to array
delete(edges.edgeCandidatesTop.count);
delete(edges.edgeCandidatesBottom.count);
if( edges.edgeCandidatesTopCount > 0){
for(var e in edges.edgeCandidatesTop){
var edge = edges.edgeCandidatesTop[e];
edgesTop.push({
distance: edge.offset,
absolute: edge.offset,
count: edge.count
});
}
}
if( edges.edgeCandidatesBottomCount > 0){
for(var e in edges.edgeCandidatesBottom){
var edge = edges.edgeCandidatesBottom[e];
edgesBottom.push({
distance: this.conf.canvas.height - edge.offset,
absolute: edge.offset,
count: edge.count
});
}
}
// sort by distance
edgesTop = edgesTop.sort((a,b) => {return a.distance - b.distance});
edgesBottom = edgesBottom.sort((a,b) => {return a.distance - b.distance});
// če za vsako stran (zgoraj in spodaj) poznamo vsaj enega kandidata, potem lahko preverimo nekaj
// stvari
if(! missingEdge ){
// predvidevamo, da je logo zgoraj ali spodaj, nikakor pa ne na obeh straneh hkrati.
// če kanal logotipa/watermarka ni vključil v video, potem si bosta razdaliji (edge.distance) prvih ključev
// zgornjega in spodnjega roba približno enaki
//
// we'll assume that no youtube channel is rude enough to put channel logo/watermark both on top and the bottom
// of the video. If logo's not included in the video, distances (edge.distance) of the first two keys should be
// roughly equal. Let's check for that.
if( edgesTop[0].distance >= edgesBottom[0].distance - alignMargin &&
edgesTop[0].distance <= edgesBottom[0].distance + alignMargin ){
var blackbarWidth = edgesTop[0].distance > edgesBottom[0].distance ?
edgesTop[0].distance : edgesBottom[0].distance;
return {
status: "ar_known",
blackbarWidth: blackbarWidth,
guardLineTop: edgesTop[0].distance,
guardLineBottom: edgesBottom[0].absolute,
top: edgesTop[0].distance,
bottom: edgesBottom[0].distance
};
}
// torej, lahko da je na sliki watermark. Lahko, da je slika samo ornh črna. Najprej preverimo za watermark
// it could be watermark. It could be a dark frame. Let's check for watermark first.
if( edgesTop[0].distance < edgesBottom[0].distance &&
edgesTop[0].count < edgesBottom[0].count &&
edgesTop[0].count < this.conf.sampleCols.length * this.settings.active.arDetect.edgeDetection.logoTreshold){
// možno, da je watermark zgoraj. Preverimo, če se kateri od drugih potencialnih robov na zgornjem robu
// ujema s prvim spodnjim (+/- variance). Če je temu tako, potem bo verjetno watermark. Logo mora imeti
// manj vzorcev kot navaden rob.
if(edgesTop[0].length > 1){
var lowMargin = edgesBottom[0].distance - alignMargin;
var highMargin = edgesBottom[0].distance + alignMargin;
for(var i = 1; i < edgesTop.length; i++){
if(edgesTop[i].distance >= lowMargin && edgesTop[i].distance <= highMargin){
// dobili smo dejanski rob. vrnimo ga
// we found the actual edge. let's return that.
var blackbarWidth = edgesTop[i].distance > edgesBottom[0].distance ?
edgesTop[i].distance : edgesBottom[0].distance;
return {
status: "ar_known",
blackbarWidth: blackbarWidth,
guardLineTop: edgesTop[i].distance,
guardLineBottom: edgesBottom[0].absolute,
top: edgesTop[i].distance,
bottom: edgesBottom[0].distance
};
}
}
}
}
if( edgesBottom[0].distance < edgesTop[0].distance &&
edgesBottom[0].count < edgesTop[0].count &&
edgesBottom[0].count <this.conf.sampleCols.length * this.settings.active.arDetect.edgeDetection.logoTreshold){
if(edgesBottom[0].length > 1){
var lowMargin = edgesTop[0].distance - alignMargin;
var highMargin = edgesTop[0].distance + alignMargin;
for(var i = 1; i < edgesBottom.length; i++){
if(edgesBottom[i].distance >= lowMargin && edgesTop[i].distance <= highMargin){
// dobili smo dejanski rob. vrnimo ga
// we found the actual edge. let's return that.
var blackbarWidth = edgesBottom[i].distance > edgesTop[0].distance ?
edgesBottom[i].distance : edgesTop[0].distance;
return {
status: "ar_known",
blackbarWidth: blackbarWidth,
guardLineTop: edgesTop[0].distance,
guardLineBottom: edgesBottom[0].absolute,
top: edgesTop[0].distance,
bottom: edgesBottom[i].distance
};
}
}
}
}
}
else{
// zgornjega ali spodnjega roba nismo zaznali. Imamo še en trik, s katerim lahko poskusimo
// določiti razmerje stranic
// either the top or the bottom edge remains undetected, but we have one more trick that we
// can try. It also tries to work around logos.
var edgeDetectionTreshold = this.conf.sampleCols.length * this.settings.active.arDetect.edgeDetection.singleSideConfirmationTreshold;
if(edges.edgeCandidatesTopCount == 0 && edges.edgeCandidatesBottomCount != 0){
for(var edge of edgesBottom){
if(edge.count >= edgeDetectionTreshold)
return {
status: "ar_known",
blackbarWidth: edge.distance,
guardLineTop: null,
guardLineBottom: edge.bottom,
top: edge.distance,
bottom: edge.distance
}
}
}
if(edges.edgeCandidatesTopCount != 0 && edges.edgeCandidatesBottomCount == 0){
for(var edge of edgesTop){
if(edge.count >= edgeDetectionTreshold)
return {
status: "ar_known",
blackbarWidth: edge.distance,
guardLineTop: edge.top,
guardLineBottom: null,
top: edge.distance,
bottom: edge.distance
}
}
}
}
// če pridemo do sem, nam ni uspelo nič. Razmerje stranic ni znano
// if we reach this bit, we have failed in determining aspect ratio. It remains unknown.
return {status: "ar_unknown"}
}
pillarTest(image){
// preverimo, če na sliki obstajajo navpične črne obrobe. Vrne 'true' če so zaznane (in če so približno enako debele), 'false' sicer.
// true vrne tudi, če zaznamo preveč črnine.
// <==XX(::::}----{::::)XX==>
// checks the image for presence of vertical pillars. Less accurate than 'find blackbar limits'. If we find a non-black object that's
// roughly centered, we return true. Otherwise we return false.
// we also return true if we detect too much black
var blackbarTreshold, upper, lower;
blackbarTreshold = this.conf.blackLevel + this.settings.active.arDetect.blackbarTreshold;
var middleRowStart = (this.conf.canvas.height >> 1) * this.conf.canvas.width;
var middleRowEnd = middleRowStart + this.conf.canvas.width - 1;
var rowStart = middleRowStart << 2;
var midpoint = (middleRowStart + (this.conf.canvas.width >> 1)) << 2
var rowEnd = middleRowEnd << 2;
var edge_left = -1, edge_right = -1;
// preverimo na levi strani
// let's check for edge on the left side
for(var i = rowStart; i < midpoint; i+=4){
if(image[i] > blackbarTreshold || image[i+1] > blackbarTreshold || image[i+2] > blackbarTreshold){
edge_left = (i - rowStart) >> 2;
break;
}
}
// preverimo na desni strani
// check on the right
for(var i = rowEnd; i > midpoint; i-= 4){
if(image[i] > blackbarTreshold || image[i+1] > blackbarTreshold || image[i+2] > blackbarTreshold){
edge_right = this.conf.canvas.width - ((i - rowStart) >> 2);
break;
}
}
// če je katerikoli -1, potem imamo preveč črnine
// we probably have too much black if either of those two is -1
if(edge_left == -1 || edge_right == -1){
return true;
}
// če sta oba robova v mejah merske napake, potem vrnemo 'false'
// if both edges resemble rounding error, we retunr 'false'
if(edge_left < this.settings.active.arDetect.pillarTest.ignoreThinPillarsPx && edge_right < this.settings.active.arDetect.pillarTest.ignoreThinPillarsPx){
return false;
}
var edgeError = this.settings.active.arDetect.pillarTest.allowMisaligned;
var error_low = 1 - edgeError;
var error_hi = 1 + edgeError;
// če sta 'edge_left' in 'edge_right' podobna/v mejah merske napake, potem vrnemo true — lahko da smo našli logo na sredini zaslona
// if 'edge_left' and 'edge_right' are similar enough to each other, we return true. If we found a logo in a black frame, we could
// crop too eagerly
if( (edge_left * error_low) < edge_right &&
(edge_left * error_hi) > edge_right ){
return true;
}
// če se ne zgodi nič od neštetega, potem nismo našli problemov
// if none of the above, we haven't found a problem
return false;
}
// pomožne funkcije
// helper functions
_columnTest(image, top, bottom, colsIn, colsOut, reverseSearchDirection){
var tmpI;
if(reverseSearchDirection){
for(var i = bottom - this.conf.canvasImageDataRowLength; i >= top; i-= this.conf.canvasImageDataRowLength){
for(var col of colsIn){
tmpI = i + (col << 2);
if( image[tmpI] > this.blackbarTreshold ||
image[tmpI + 1] > this.blackbarTreshold ||
image[tmpI + 2] > this.blackbarTreshold ){
var bottom = (i / this.conf.canvasImageDataRowLength) + 1;
colsOut.push({
col: col,
bottom: bottom
});
colsIn.splice(colsIn.indexOf(col), 1);
}
}
if(colsIn.length < this.colsTreshold)
break;
}
} else {
for(var i = top; i < bottom; i+= this.conf.canvasImageDataRowLength){
for(var col of colsIn){
tmpI = i + (col << 2);
if( image[tmpI] > this.blackbarTreshold ||
image[tmpI + 1] > this.blackbarTreshold ||
image[tmpI + 2] > this.blackbarTreshold ){
colsOut.push({
col: col,
top: (i / this.conf.canvasImageDataRowLength) - 1
});
colsIn.splice(colsIn.indexOf(col), 1);
}
}
if(colsIn.length < this.colsTreshold)
break;
}
}
}
_columnTest_dbgc(image, top, bottom, colsIn, colsOut, reverseSearchDirection){
var tmpI;
if(reverseSearchDirection){
for(var i = bottom - this.conf.canvasImageDataRowLength; i >= top; i-= this.conf.canvasImageDataRowLength){
for(var col of colsIn){
tmpI = i + (col << 2);
if( image[tmpI] > this.blackbarTreshold ||
image[tmpI + 1] > this.blackbarTreshold ||
image[tmpI + 2] > this.blackbarTreshold ){
var bottom = (i / this.conf.canvasImageDataRowLength) + 1;
colsOut.push({
col: col,
bottom: bottom
});
colsIn.splice(colsIn.indexOf(col), 1);
this.conf.debugCanvas.trace(tmpI,DebugCanvasClasses.EDGEDETECT_CANDIDATE);
}
else{
this.conf.debugCanvas.trace(tmpI, DebugCanvasClasses.EDGEDETECT_ONBLACK);
}
}
if(colsIn.length < this.colsTreshold)
break;
}
} else {
for(var i = top; i < bottom; i+= this.conf.canvasImageDataRowLength){
for(var col of colsIn){
tmpI = i + (col << 2);
if( image[tmpI] > this.blackbarTreshold ||
image[tmpI + 1] > this.blackbarTreshold ||
image[tmpI + 2] > this.blackbarTreshold ){
colsOut.push({
col: col,
top: (i / this.conf.canvasImageDataRowLength) - 1
});
colsIn.splice(colsIn.indexOf(col), 1);
this.conf.debugCanvas.trace(tmpI, DebugCanvasClasses.EDGEDETECT_CANDIDATE);
if(tmpI-1 > 0){
this.conf.debugCanvas.trace(tmpI - 1, DebugCanvasClasses.EDGEDETECT_CANDIDATE_SECONDARY);
}
if(tmpI+1 < image.length){
this.conf.debugCanvas.trace(tmpI + 1, DebugCanvasClasses.EDGEDETECT_CANDIDATE_SECONDARY);
}
} else {
this.conf.debugCanvas.trace(tmpI, DebugCanvasClasses.EDGEDETECT_ONBLACK);
}
}
if(colsIn.length < this.colsTreshold)
break;
}
}
}
_blackbarTest(image, start, end){
for(var i = start; i < end; i += 4){
if( image[i ] > this.blackbarTreshold ||
image[i+1] > this.blackbarTreshold ||
image[i+2] > this.blackbarTreshold ){
return true;
}
}
return false; // no violation
}
_blackbarTest_dbg(image, start, end){
for(var i = start; i < end; i += 4){
if( image[i ] > this.blackbarTreshold ||
image[i+1] > this.blackbarTreshold ||
image[i+2] > this.blackbarTreshold ){
this.conf.debugCanvas.trace(i, DebugCanvasClasses.VIOLATION)
return true;
} else {
this.conf.debugCanvas.trace(i, DebugCanvasClasses.EDGEDETECT_BLACKBAR)
}
}
return false; // no violation
}
_imageTest(image, start, end, sampleOffset, edgeCandidates){
var detections = 0;
for(var i = start; i < end; i += 4){
if( image[i ] > this.blackbarTreshold ||
image[i+1] > this.blackbarTreshold ||
image[i+2] > this.blackbarTreshold ){
++detections;
}
}
if(detections >= this.detectionTreshold){
if(edgeCandidates[sampleOffset] != undefined)
edgeCandidates[sampleOffset].count++;
else{
edgeCandidates[sampleOffset] = {offset: sampleOffset, count: 1};
edgeCandidates.count++;
}
}
}
_imageTest_dbg(image, start, end, sampleOffset, edgeCandidates){
var detections = 0;
for(var i = start; i < end; i += 4){
if( image[i ] > this.blackbarTreshold ||
image[i+1] > this.blackbarTreshold ||
image[i+2] > this.blackbarTreshold ){
++detections;
this.conf.debugCanvas.trace(i, DebugCanvasClasses.EDGEDETECT_IMAGE);
} else {
this.conf.debugCanvas.trace(i, DebugCanvasClasses.WARN);
}
}
if(detections >= this.detectionTreshold){
if(edgeCandidates[sampleOffset] != undefined)
edgeCandidates[sampleOffset].count++;
else{
edgeCandidates[sampleOffset] = {offset: sampleOffset, count: 1};
edgeCandidates.count++;
}
}
}
}

View File

@ -1,301 +0,0 @@
class Settings {
constructor(activeSettings, updateCallback) {
this.active = activeSettings ? activeSettings : undefined;
this.default = ExtensionConf;
this.useSync = false;
this.version = undefined;
this.updateCallback = updateCallback;
const ths = this;
if(BrowserDetect.firefox) {
browser.storage.onChanged.addListener( (changes, area) => {
if (Debug.debug && Debug.debugStorage) {
console.log("[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) {
console.log("[Settings::<storage/on change>] new settings object:", JSON.parse(changes.uwSettings.newValue));
}
}
if(changes['uwSettings'] && changes['uwSettings'].newValue) {
ths.setActive(JSON.parse(changes.uwSettings.newValue));
}
if(this.updateCallback) {
try {
updateCallback();
} catch (e) {
console.log("[Settings] CALLING UPDATE CALLBACK FAILED.")
}
}
});
} else if (BrowserDetect.chrome) {
chrome.storage.onChanged.addListener( (changes, area) => {
if (Debug.debug && Debug.debugStorage) {
console.log("[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) {
console.log("[Settings::<storage/on change>] new settings object:", JSON.parse(changes.uwSettings.newValue));
}
}
if(changes['uwSettings'] && changes['uwSettings'].newValue) {
ths.setActive(JSON.parse(changes.uwSettings.newValue));
}
if(this.updateCallback) {
try {
updateCallback();
} catch (e) {
console.log("[Settings] CALLING UPDATE CALLBACK FAILED.")
}
}
});
}
}
async init() {
const settings = await this.get();
if(Debug.debug) {
console.log("[Settings::init] Configuration fetched from storage:", settings);
}
// if there's no settings saved, return default settings.
if(! settings || (Object.keys(settings).length === 0 && settings.constructor === Object)) {
this.setDefaultSettings();
this.active = this.getDefaultSettings();
return this.active;
}
// if there's settings, set saved object as active settings
this.active = settings;
// check if extension has been updated. If not, return settings as they were retreived
if (BrowserDetect.firefox) {
this.version = browser.runtime.getManifest().version;
} else if (BrowserDetect.chrome) {
this.version = chrome.runtime.getManifest().version;
} else if (BrowserDetect.edge) {
this.version = browser.runtime.getManifest().version;
}
if(settings.version === this.version) {
if(Debug.debug) {
console.log("[Settings::init] extension was saved with current version of ultrawidify (", this.version, "). Returning object as-is.");
}
return this.active;
}
// 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);
if(Debug.debug) {
console.log("[Settings.init] Results from ObjectCopy.addNew()?", patched, "\n\nSettings from storage", settings, "\ndefault?", this.default,);
}
if(patched){
this.active = patched;
} else {
this.active = JSON.parse(JSON.stringify(this.default));
}
this.set(this.active);
return this.active;
}
async get() {
if (BrowserDetect.firefox || BrowserDetect.edge) {
const ret = this.useSync ? await browser.storage.sync.get('uwSettings') : await browser.storage.local.get('uwSettings');
try {
return JSON.parse(ret.uwSettings);
} catch(e) {
return undefined;
}
} else if (BrowserDetect.chrome) {
const ret = new Promise( (resolve, reject) => {
chrome.storage.sync.get('uwSettings', (res) => resolve(res));
});
return ret['uwSettings'];
}
}
async set(extensionConf) {
if (Debug.debug) {
console.log("[Settings::set] setting new settings:", extensionConf)
}
if (BrowserDetect.firefox || BrowserDetect.edge) {
extensionConf.version = this.version;
return this.useSync ? browser.storage.sync.set( {'uwSettings': JSON.stringify(extensionConf)}): browser.storage.local.set( {'uwSettings': JSON.stringify(extensionConf)});
} else if (BrowserDetect.chrome) {
return chrome.storage.sync.set( {'uwSettings': JSON.stringify(extensionConf)});
}
}
async setActive(activeSettings) {
this.active = activeSettings;
}
async setProp(prop, value) {
this.active[prop] = value;
}
async save() {
if (Debug.debug) {
console.log("[Settings::save] Saving active settings:", this.active);
}
this.set(this.active);
}
getDefaultSettings() {
return JSON.parse(JSON.stringify(this.default));
}
setDefaultSettings() {
this.set(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
// * default — allow if default is to allow, block if default is to block
// * disabled — never allow
getSiteSettings(site) {
if (!site) {
site = window.location.hostname;
}
if (!site || !this.active.sites[site]) {
return {};
}
return this.active.sites[site];
}
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) {
console.log("[Settings::canStartExtension] window.location.hostname is null or undefined:", window.location.hostname)
console.log("active 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]) {
return this.active.extensionMode === "blacklist";
}
if(this.active.extensionMode === "blacklist") {
return this.active.sites[site].status !== "disabled";
} else if (this.active.extensionMode === "whitelist") {
return this.active.sites[site].status === "enabled";
} else {
return false;
}
}catch(e){
if(Debug.debug){
console.log("[Settings.js::canStartExtension] Something went wrong — are settings defined/has init() been called?\nSettings object:", this)
}
return false;
}
}
extensionEnabled(){
return this.active.extensionMode !== 'disabled'
}
extensionEnabledForSite(site) {
return this.canStartExtension(site);
}
canStartAutoAr(site) {
if (!site) {
site = window.location.hostname;
if (!site) {
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;
console.log("[Settings::canStartAutoAr] ----------------\nCAN WE START THIS EXTENSION ON SITE", site,
"?\n\nsettings.active.sites[site]=", this.active.sites[site],
"\nExtension mode?", this.active.arDetect.mode,
"\nCan extension be started?", csar
);
}
// if site is not defined, we use default mode:
if (! this.active.sites[site]) {
return this.active.arDetect.mode === "blacklist";
}
if (this.active.arDetect.mode === "blacklist") {
return this.active.sites[site].arStatus !== "disabled";
} else if (this.active.arDetect.mode === "whitelist") {
return this.active.sites[site].arStatus === "enabled";
} else {
return false;
}
}
getDefaultAr(site) {
site = this.getSiteSettings(site);
if (site.defaultAr) {
return site.defaultAr;
}
return this.active.miscFullscreenSettings.defaultAr;
}
getDefaultStretchMode(site) {
site = this.getSiteSettings(site);
if (site.stretch) {
return site.stretch;
}
return this.active.stretch.initialMode;
}
getDefaultVideoAlignment(site) {
site = this.getSiteSettings(site);
if (site.videoAlignment) {
return site.videoAlignment;
}
return this.active.miscFullscreenSettings.videoFloat;
}
}

View File

@ -1,885 +0,0 @@
class ArDetector {
constructor(videoData){
this.conf = videoData;
this.video = videoData.video;
this.settings = videoData.settings;
this.setupTimer = null;
this.timer = null;
this.sampleCols = [];
// todo: dynamically detect the following two
this.canFallback = true;
this.fallbackMode = false;
this.blackLevel = this.settings.active.arDetect.blackLevel_default;
this.arid = (Math.random()*100).toFixed();
if (Debug.init) {
console.log("[ArDetector::ctor] creating new ArDetector. arid:", this.arid);
}
}
init(){
if (Debug.debug || Debug.init) {
console.log("[ArDetect::init] Initializing autodetection. arid:", this.arid);
}
this.setup(this.settings.active.arDetect.hSamples, this.settings.active.arDetect.vSamples);
}
destroy(){
if(Debug.debug || Debug.init) {
console.log(`[ArDetect::destroy] <arid:${this.arid}>`)
}
this.debugCanvas.destroy();
this.stop();
}
setup(cwidth, cheight, forceStart){
this.guardLine = new GuardLine(this);
this.edgeDetector = new EdgeDetect(this);
this.debugCanvas = new DebugCanvas(this);
if(Debug.debug || Debug.init) {
console.log("[ArDetect::setup] Starting autodetection setup. arid:", this.arid);
}
if (this.fallbackMode || cheight !== this.settings.active.arDetect.hSamples) {
if(Debug.debug) {
console.log("%c[ArDetect::setup] WARNING: CANVAS RESET DETECTED - recalculating guardLine", "background: #000; color: #ff2" )
}
// blackbar, imagebar
this.guardLine.reset();
}
if(!cwidth){
cwidth = this.settings.active.arDetect.hSamples;
cheight = this.settings.active.arDetect.vSamples;
}
try{
if(Debug.debug){
console.log("[ArDetect::setup] Trying to setup automatic aspect ratio detector. Choice config bits:\ncanvas dimensions:",cwidth, "×", cheight, "\nvideoData:", this.conf);
}
this._halted = false;
this.detectionTimeoutEventCount = 0;
// // vstavimo začetne stolpce v this.sampleCols. - NE!
// // let's insert initial columns to this.sampleCols - NO!!! do it later dow
// this.sampleCols = [];
// var samplingIntervalPx = parseInt(cheight / this.settings.active.arDetect.samplingInterval)
// for(var i = 1; i < this.settings.active.arDetect.samplingInterval; i++){
// this.sampleCols.push(i * samplingIntervalPx);
// }
if(this.canvas){
if(Debug.debug)
console.log("[ArDetect::setup] existing canvas found. REMOVING KEBAB removing kebab\n\n\n\n(im hungry and you're not authorized to have it)");
this.canvas.remove();
if(Debug.debug)
console.log("[ArDetect::setup] canvas removed");
}
// imamo video, pa tudi problem. Ta problem bo verjetno kmalu popravljen, zato setup začnemo hitreje kot prej
// we have a video, but also a problem. This problem will prolly be fixed very soon, so setup is called with
// less delay than before
if(this.video.videoWidth === 0 || this.video.videoHeight === 0 ){
if(Debug.debug){
console.log("[ArDetector::setup] video has no width or height!", this.video.videoWidth,"×", this.video.videoHeight)
}
this.scheduleInitRestart();
return;
}
// 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.context = this.canvas.getContext("2d");
// do setup once
// tho we could do it for every frame
this.canvasScaleFactor = cheight / this.video.videoHeight;
try{
// determine where to sample
var ncol = this.settings.active.arDetect.staticSampleCols;
var nrow = this.settings.active.arDetect.staticSampleRows;
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);
}
}
}
catch(ex){
console.log("%c[ArDetect::_arSetup] something went terribly wrong when calcuating sample colums.", this.settings.active.colors.criticalFail);
console.log("settings object:", Settings);
console.log("error:", ex);
}
// we're also gonna reset this
this.guardLine.top = null;
this.guardLine.bottom = null;
this.resetBlackLevel();
this._forcehalt = false;
// if we're restarting ArDetect, we need to do this in order to force-recalculate aspect ratio
this.conf.resizer.setLastAr({type: "auto", ar: null});
this.canvasImageDataRowLength = cwidth << 2;
this.noLetterboxCanvasReset = false;
if(forceStart || this.settings.canStartAutoAr() ) {
this.start();
}
}
catch(ex){
console.log(ex);
}
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 (Debug.debug) {
console.log("%c[ArDetect::setup] Starting automatic aspect ratio detection.", _ard_console_start);
}
this._halted = false;
this.conf.resizer.resetLastAr();
this.scheduleFrameCheck(0, true);
}
unpause() {
if(this._paused){ // resume only if we explicitly paused
this._paused = false;
this.start();
}
}
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;
this.stop();
}
}
stop(){
if(Debug.debug){
console.log("%c[ArDetect::_ard_stop] Stopping automatic aspect ratio detection", _ard_console_stop);
}
this._forcehalt = true;
this._halted = true;
clearTimeout(this.setupTimer);
clearTimeout(this.timer);
this.conf.resizer.resetLastAr();
}
isRunning(){
return ! this._halted && ! this._paused;
}
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.init();
}catch(e){console.log("[ArDetector::scheduleInitRestart] Failed to start init(). Error:",e)}
ths = null;
},
timeout
);
}
async scheduleFrameCheck(timeout, force_reset){
if(! timeout){
this.frameCheck();
return;
}
var e = await this.settings.get();
// run anything that needs to be run after frame check
this.postFrameCheck();
// don't allow more than 1 instance
if(this.timer){
clearTimeout(this.timer);
}
var ths = this;
// console.log(this.video, "this.video | ths.video", ths.video)
// console.log(this.conf.video, "this.conf | ths.conf", ths.conf.video)
// console.log("resizer conf&vid",
// this.conf.resizer, this.conf.resizer.conf, this.conf.resizer.video, this.conf.resizer.conf.video )
// debugger;
this.timer = setTimeout(function(){
ths.timer = null;
try{
ths.frameCheck();
}catch(e){console.log("Frame check failed. Error:",e)}
ths = null;
},
timeout
);
}
postFrameCheck(){
if(Debug.debugCanvas.enabled){
this.debugCanvas.update();
}
}
//#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(){
if(Debug.debug)
console.log("%c[ArDetect::_ard_canvasReadyForDrawWindow] (?)", "color: #44f", this.canvas.height == window.innerHeight, "(ard_height:", this.canvas.height, "| window height:", window.innerHeight, ")");
return this.canvas.height == window.innerHeight
}
getTimeout(baseTimeout, startTime){
var execTime = (performance.now() - startTime);
if( execTime > this.settings.active.arDetect.autoDisable.maxExecutionTime ){
// this.detectionTimeoutEventCount++;
if(Debug.debug){
console.log("[ArDetect::getTimeout] Exec time exceeded maximum allowed execution time. This has now happened " + this.detectionTimeoutEventCount + " times in a row.");
}
// if( this.detectionTimeoutEventCount >= this.settings.active.arDetect.autoDisable.consecutiveTimeoutCount ){
// if (Debug.debug){
// console.log("[ArDetect::getTimeout] Maximum execution time was exceeded too many times. Automatic aspect ratio detection has been disabled.");
// }
// Comms.sendToBackgroundScript({cmd: 'disable-autoar', reason: 'Automatic aspect ratio detection was taking too much time and has been automatically disabled in order to avoid lag.'});
// _ard_stop();
// return 999999;
// }
} else {
this.detectionTimeoutEventCount = 0;
}
// return baseTimeout > this.settings.active.arDetect.minimumTimeout ? baseTimeout : this.settings.active.arDetect.minimumTimeout;
return baseTimeout;
}
//#endregion
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;
edge.left = 0;
edges.right = 0;
}
let zoomFactor = 1;
var letterbox = edges.top + edges.bottom;
if (this.fallbackMode) {
// 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(this.fallbackMode){
if(edges.top > 1 && edges.top <= this.settings.active.arDetect.fallbackMode.noTriggerZonePx ){
if(Debug.debug && Debug.debugArDetect) {
console.log("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){
let actualHeight = 0; // purely for fallback mode
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 == "auto" && lastAr.ar != null){
// 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
if(Debug.debug && Debug.debugArDetect)
console.log("%c[ArDetect::_ard_processAr] 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){
if(Debug.debug && Debug.debugArDetect)
console.log("%c[ArDetect::_ard_processAr] aspect ratio change denied — diff %:", "background: #740; color: #fa2", arDiff_percent)
return;
}
else if(Debug.debug && Debug.debugArDetect){
console.log("%c[ArDetect::_ard_processAr] aspect ratio change accepted — diff %:", "background: #153; color: #4f9", arDiff_percent)
}
}
if(Debug.debug)
console.log("[ArDetect::_ard_processAr] attempting to fix aspect ratio. New aspect ratio: ", trueAr);
this.conf.resizer.setAr(trueAr, {type: "auto", ar: trueAr});
}
frameCheck(){
// console.log("this.video:", this.video, this.conf.video);
// debugger;
if(this._halted)
return;
if(! this.video){
if(Debug.debug || Debug.warnings_critical)
console.log("[ArDetect::_ard_vdraw] Video went missing. Destroying current instance of videoData.")
this.conf.destroy();
return;
}
var fallbackMode = false;
var startTime = performance.now();
var baseTimeout = this.settings.active.arDetect.timer_playing;
var triggerTimeout;
var guardLineResult = true; // true if success, false if fail. true by default
var imageDetectResult = false; // true if we detect image along the way. false by default
var sampleCols = this.sampleCols.slice(0);
var how_far_treshold = 8; // how much can the edge pixel vary (*4)
if(this.video.ended ){
// we slow down if ended. Detecting is pointless.
this.scheduleFrameCheck(this.settings.active.arDetect.timer_paused);
return false;
}
if(this.video.paused){
// č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.
baseTimeout = this.settings.active.arDetect.timer_paused;
}
try{
this.context.drawImage(this.video, 0,0, this.canvas.width, this.canvas.height);
}
catch(ex){
if(Debug.debug) {
console.log("%c[ArDetect::_ard_vdraw] can't draw image on canvas. Trying canvas.drawWindow instead", "color:#000; backgroud:#f51;", ex);
}
try{
if(! this.settings.active.arDetect.fallbackMode.enabled)
throw "fallbackMode is disabled.";
if(this.canvasReadyForDrawWindow()){
this.context.drawWindow(window, this.canvasDrawWindowHOffset, 0, this.canvas.width, this.canvas.height, "rgba(0,0,128,1)");
if(Debug.debug)
console.log("%c[ArDetect::_ard_vdraw] canvas.drawImage seems to have worked", "color:#000; backgroud:#2f5;");
this.fallbackMode = true;
}
else{
// canvas needs to be resized, so let's change setup
this.stop();
var newCanvasWidth = window.innerHeight * (this.video.videoWidth / this.video.videoHeight);
var newCanvasHeight = window.innerHeight;
if (this.conf.resizer.videoFloat === "center") {
this.canvasDrawWindowHOffset = Math.round((window.innerWidth - newCanvasWidth) * 0.5);
} else if (this.conf.resizer.videoFloat == "left") {
this.canvasDrawWindowHOffset = 0;
} else {
this.canvasDrawWindowHOffset = window.innerWidth - newCanvasWidth;
}
this.setup(newCanvasWidth, newCanvasHeight);
return;
}
}
catch(ex){
if(Debug.debug)
console.log("%c[ArDetect::_ard_vdraw] okay this didnt work either", "color:#000; backgroud:#f51;", ex);
this.scheduleFrameCheck( this.settings.active.arDetect.timer_error );
return;
}
}
if (! this.blackLevel) {
if(Debug.debugArDetect)
console.log("[ArDetect::_ard_vdraw] black level undefined, resetting");
this.resetBlackLevel();
}
// we get the entire frame so there's less references for garbage collection to catch
var image = this.context.getImageData(0, 0, this.canvas.width, this.canvas.height).data;
if(Debug.debugCanvas.enabled){
this.debugCanvas.setBuffer(image);
}
//#region black level detection
// fast test to see if aspect ratio is correct. If we detect anything darker than blackLevel, we modify
// blackLevel to the new lowest value
var isLetter=true;
var currentMaxVal = 0;
var currentMax_a;
var currentMinVal = 48; // not 255 cos safety, even this is prolly too high
var currentMin_a;
var rowOffset = 0;
var colOffset_r, colOffset_g, colOffset_b;
// detect black level. if currentMax and currentMin vary too much, we automatically know that
// black level is bogus and that we aren't letterboxed. We still save the darkest value as black level,
// though — as black bars will never be brighter than that.
for(var i = 0; i < sampleCols.length; ++i){
colOffset_r = sampleCols[i] << 2;
colOffset_g = colOffset_r + 1;
colOffset_b = colOffset_r + 2;
currentMax_a = image[colOffset_r] > image[colOffset_g] ? image[colOffset_r] : image[colOffset_g];
currentMax_a = currentMax_a > image[colOffset_b] ? currentMax_a : image[colOffset_b];
currentMaxVal = currentMaxVal > currentMax_a ? currentMaxVal : currentMax_a;
currentMin_a = image[colOffset_r] < image[colOffset_g] ? image[colOffset_r] : image[colOffset_g];
currentMin_a = currentMin_a < image[colOffset_b] ? currentMin_a : image[colOffset_b];
currentMinVal = currentMinVal < currentMin_a ? currentMinVal : currentMin_a;
}
// we'll shift the sum. math says we can do this
rowOffset = this.canvas.width * (this.canvas.height - 1);
for(var i = 0; i < sampleCols.length; ++i){
colOffset_r = (rowOffset + sampleCols[i]) << 2;
colOffset_g = colOffset_r + 1;
colOffset_b = colOffset_r + 2;
currentMax_a = image[colOffset_r] > image[colOffset_g] ? image[colOffset_r] : image[colOffset_g];
currentMax_a = currentMax_a > image[colOffset_b] ? currentMax_a : image[colOffset_b];
currentMaxVal = currentMaxVal > currentMax_a ? currentMaxVal : currentMax_a;
currentMin_a = image[colOffset_r] < image[colOffset_g] ? image[colOffset_r] : image[colOffset_g];
currentMin_a = currentMin_a < image[colOffset_b] ? currentMin_a : image[colOffset_b];
if(currentMinVal == undefined && currenMinVal != undefined)
currentMinVal = currentMin_a;
else if(currentMin_a != undefined)
currentMinVal = currentMinVal < currentMin_a ? currentMinVal : currentMin_a;
}
// save black level only if defined
if(currentMinVal)
this.blackLevel = this.blackLevel < currentMinVal ? this.blackLevel : currentMinVal;
//#endregion
// this means we don't have letterbox
if ( currentMaxVal > (this.blackLevel + this.settings.active.arDetect.blackbarTreshold) || (currentMaxVal - currentMinVal) > this.settings.active.arDetect.blackbarTreshold*4 ){
// Č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.
if(Debug.debug && Debug.debugArDetect){
console.log(`%c[ArDetect::_ard_vdraw] ---- NO EDGE DETECTED! — canvas has no edge. ----\ncurrentMaxVal: ${currentMaxVal}\nBlack level (+ treshold):${this.blackLevel} (${this.blackLevel + this.settings.active.arDetect.blackbarTreshold})\n---diff test---\nmaxVal-minVal: ${ (currentMaxVal - currentMinVal)}\ntreshold: ${this.settings.active.arDetect.blackbarTreshold}`, "color: #aaf");
}
// Pogledamo, ali smo že kdaj ponastavili CSS. Če še nismo, potem to storimo. Če smo že, potem ne.
// Ponastavimo tudi guardline (na null).
// let's chec if we ever reset CSS. If we haven't, then we do so. If we did, then we don't.
// while resetting the CSS, we also reset guardline top and bottom back to null.
if(! this.noLetterboxCanvasReset){
this.conf.resizer.reset({type: "auto", ar: null});
this.guardLine.reset();
this.noLetterboxCanvasReset = true;
}
triggerTimeout = this.getTimeout(baseTimeout, startTime);
this.scheduleFrameCheck(triggerTimeout); //no letterbox, no problem
return;
}
if(Debug.debug && Debug.debugArDetect){
console.log(`%c[ArDetect::_ard_vdraw] edge was detected. Here are stats:\ncurrentMaxVal: ${currentMaxVal}\nBlack level (+ treshold):${this.blackLevel} (${this.blackLevel + this.settings.active.arDetect.blackbarTreshold})\n---diff test---\nmaxVal-minVal: ${ (currentMaxVal - currentMinVal)}\ntreshold: ${this.settings.active.arDetect.blackbarTreshold}`, "color: #afa");
}
// Č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;
// let's do a quick test to see if we're on a black frame
// TODO: reimplement but with less bullshit
// poglejmo, če obrežemo preveč.
// let's check if we're cropping too much (or whatever)
var guardLineOut;
guardLineOut = this.guardLine.check(image, this.fallbackMode);
if (guardLineOut.blackbarFail) { // add new ssamples to our sample columns
for(var col of guardLineOut.offenders){
sampleCols.push(col)
}
}
// če ni padla nobena izmed funkcij, potem se razmerje stranic ni spremenilo
// if both succeed, then aspect ratio hasn't changed.
// if we're in fallback mode and blackbar test failed, we restore CSS
if (this.fallbackMode && guardLineOut.blackbarFail) {
this.conf.resizer.reset({type: "auto", ar: null});
this.guardLine.reset();
this.noLetterboxCanvasReset = true;
triggerTimeout = this.getTimeout(baseTimeout, startTime);
this.scheduleFrameCheck(triggerTimeout); //no letterbox, no problem
return;
}
if (!guardLineOut.imageFail && !guardLineOut.blackbarFail) {
if(Debug.debug && Debug.debugArDetect){
console.log(`%c[ArDetect::_ard_vdraw] guardLine tests were successful. (no imagefail and no blackbarfail)\n`, "color: #afa", guardLineOut);
}
triggerTimeout = this.getTimeout(baseTimeout, startTime);
this.scheduleFrameCheck(triggerTimeout); //no letterbox, no problem
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(image, null, EdgeDetectPrimaryDirection.HORIZONTAL).status === 'ar_known'){
if(Debug.debug && guardLineOut.blackbarFail){
console.log("[ArDetect::_ard_vdraw] Detected blackbar violation and pillarbox. Resetting to default aspect ratio.");
}
if(guardLineOut.blackbarFail){
this.conf.resizer.reset({type: "auto", ar: null});
this.guardLine.reset();
}
triggerTimeout = this.getTimeout(baseTimeout, startTime);
this.scheduleFrameCheck(triggerTimeout);
return;
}
}
} catch(e) {
if(Debug.debug) {
console.log("[ArDetect.js::frameCheck] something went wrong when 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(image, sampleCols, EdgeDetectPrimaryDirection.VERTICAL, EdgeDetectQuality.IMPROVED, guardLineOut);
if(Debug.debug && Debug.debugArDetect){
console.log(`%c[ArDetect::_ard_vdraw] edgeDetector returned this\n`, "color: #aaf", edgePost);
}
// console.log("SAMPLES:", blackbarSamples, "candidates:", edgeCandidates, "post:", edgePost,"\n\nblack level:", this.blackLevel, "tresh:", this.blackLevel + this.settings.active.arDetect.blackbarTreshold);
if(edgePost.status == "ar_known"){
// zaznali smo rob — vendar pa moramo pred obdelavo še preveriti, ali ni "rob" slučajno besedilo. Če smo kot rob pofočkali
// besedilo, potem to ni veljaven rob. Razmerja stranic se zato ne bomo pipali.
// we detected an edge — but before we process it, we need to check if the "edge" isn't actually some text. If the detected
// edge is actually some text on black background, we shouldn't touch the aspect ratio. Whatever we detected is invalid.
// var textEdge = false;;
// if(edgePost.guardLineTop != null){
// var row = edgePost.guardLineTop + ~~(this.canvas.height * this.settings.active.arDetect.textLineTest.testRowOffset);
// textEdge |= textLineTest(image, row);
// }
// if(edgePost.guardLineTop != null){
// var row = edgePost.guardLineTop - ~~(this.canvas.height * this.settings.active.arDetect.textLineTest.testRowOffset);
// textEdge |= textLineTest(image, row);
// }
// v nekaterih common-sense izjemah ne storimo ničesar
var newAr = this.calculateArFromEdges(edgePost);
if (this.fallbackMode
&& (!guardLineOut.blackbarFail && guardLineOut.imageFail)
&& newAr < this.conf.resizer.getLastAr().ar
) {
// V primeru nesmiselnih rezultatov tudi ne naredimo ničesar.
// v fallback mode se lahko naredi, da je novo razmerje stranice ožje kot staro, kljub temu da je šel
// blackbar test skozi. Spremembe v tem primeru ne dovolimo.
//
// (Pravilen fix? Popraviti je treba računanje robov. V fallback mode je treba upoštevati, da obrobe,
// ki smo jih obrezali, izginejo is canvasa)
//
// NOTE: pravilen fix je bil implementiran
triggerTimeout = this.getTimeout(baseTimeout, startTime);
this.scheduleFrameCheck(triggerTimeout);
return;
}
// if(!textEdge){
if(Debug.debug && Debug.debugArDetect){
console.log(`%c[ArDetect::_ard_vdraw] Triggering aspect ration change! new ar: ${newAr}`, "color: #aaf");
}
this.processAr(newAr);
// we also know edges for guardline, so set them.
// we need to be mindful of fallbackMode though
if (!this.fallbackMode) {
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
})
}
}
// }
// else{
// console.log("detected text on edges, dooing nothing")
// }
triggerTimeout = this.getTimeout(baseTimeout, startTime);
this.scheduleFrameCheck(triggerTimeout);
return;
} else {
triggerTimeout = this.getTimeout(baseTimeout, startTime);
this.scheduleFrameCheck(triggerTimeout); //no letterbox, no problem
return;
}
}
resetBlackLevel(){
this.blackLevel = this.settings.active.arDetect.blackLevel_default;
}
}
if(Debug.debug)
console.log("Loading: ArDetect");
var _ard_console_stop = "background: #000; color: #f41";
var _ard_console_start = "background: #000; color: #00c399";
var textLineTest = function(image, row){
// preverimo, če vrstica vsebuje besedilo na črnem ozadju. Če ob pregledu vrstice naletimo na veliko sprememb
// iz črnega v ne-črno, potem obstaja možnost, da gledamo besedilo. Prisotnost take vrstice je lahko znak, da
// zaznano razmerje stranic ni veljavno
//
// vrne 'true' če zazna text, 'false' drugače.
//
//
// check if line contains any text. If line scan reveals a lot of changes from black to non-black there's a
// chance we're looking at text on a black background. If we detect text near what we think is an edge of the
// video, there's a good chance we're about to incorrectly adjust the aspect ratio.
//
// returns 'true' if text is detected, 'false' otherwise
var blackbarTreshold = this.blackLevel + this.settings.active.arDetect.blackbarTreshold;
var nontextTreshold = this.canvas.width * this.settings.active.arDetect.textLineTest.nonTextPulse;
var rowStart = (row * this.canvas.width) << 2;
var rowEnd = rowStart + (this.canvas.width << 2);
var pulse = false;
var currentPulseLength = 0, pulseCount = 0;
var pulses = [];
var longestBlack = 0;
// preglejmo vrstico
// analyse the row
for(var i = rowStart; i < rowEnd; i+= 4){
if(pulse){
if(image[i] < blackbarTreshold || image[i+1] < blackbarTreshold || image[i+2] < blackbarTreshold){
// pulses.push(currentPulseLength);
pulseCount++;
pulse = false;
currentPulseLength = 0;
}
else{
currentPulseLength++;
// če najdemo dovolj dolgo zaporedje ne-črnih točk, potem vrnemo 'false' — dobili smo legitimen rob
// if we find long enough uninterrupted line of non-black point, we fail the test. We found a legit edge.
if(currentPulseLength > nontextTreshold){
return false;
}
}
}
else{
if(image[i] > blackbarTreshold || image[i+1] > blackbarTreshold || image[i+2] > blackbarTreshold){
if(currentPulseLength > longestBlack){
longestBlack = currentPulseLength;
}
pulse = true;
currentPulseLength = 0;
}
else{
currentPulseLength++;
}
}
}
if(pulse){
pulseCount++;
// pulses.push(currentPulseLength);
}
// pregledamo rezultate:
// analyse the results
// console.log("pulse test:\n\npulses:", pulseCount, "longest black:", longestBlack);
// če smo zaznali dovolj pulzov, potem vrnemo res
// if we detected enough pulses, we return true
if(pulseCount > this.settings.active.arDetect.textLineTest.pulsesToConfirm){
return true;
}
// če je najdaljša neprekinjena črta črnih pikslov širša od polovice širine je merilo za zaznavanje
// besedila rahlo milejše
// if the longest uninterrupted line of black pixels is wider than half the width, we use a more
// forgiving standard for determining if we found text
if( longestBlack > (this.canvas.width >> 1) &&
pulseCount > this.settings.active.arDetect.textLineTest.pulsesToConfirmIfHalfBlack ){
return true;
}
// če pridemo do sem, potem besedilo ni bilo zaznano
// if we're here, no text was detected
return false;
}

View File

@ -1,294 +0,0 @@
if(Debug.debug)
console.log("Loading: PageInfo.js");
class PageInfo {
constructor(comms, settings){
this.hasVideos = false;
this.siteDisabled = false;
this.videos = [];
this.settings = settings;
this.lastUrl = window.location.href;
this.rescan(RescanReason.PERIODIC);
this.scheduleUrlCheck();
if(comms){
this.comms = comms;
}
if(this.videos.length > 0){
console.log("registering video")
comms.registerVideo();
}
this.currentZoomScale = 1;
}
destroy() {
if(Debug.debug || Debug.init){
console.log("[PageInfo::destroy] destroying all videos!")
}
if(this.rescanTimer){
clearTimeout(this.rescanTimer);
}
for (var video of this.videos) {
video.destroy();
}
}
reset() {
for(var video of this.videos) {
video.destroy();
}
this.rescan(RescanReason.MANUAL);
}
rescan(rescanReason){
try{
var vids = document.getElementsByTagName('video');
if(!vids || vids.length == 0){
this.hasVideos = false;
if(rescanReason == RescanReason.PERIODIC){
this.scheduleRescan(RescanReason.PERIODIC);
}
return;
}
// add new videos
this.hasVideos = false;
var videoExists = false;
var video, v;
for (video of vids) {
// če najdemo samo en video z višino in širino, to pomeni, da imamo na strani veljavne videe
// če trenutni video nima definiranih teh vrednostih, preskočimo vse nadaljnja preverjanja
// <===[:::::::]===>
// if we find even a single video with width and height, that means the page has valid videos
// if video lacks either of the two properties, we skip all further checks cos pointless
if(video.offsetWidth && video.offsetHeight){
this.hasVideos = true;
} else {
continue;
}
videoExists = false;
for (v of this.videos) {
if (v.destroyed) {
continue; //TODO: if destroyed video is same as current video, copy aspect ratio settings to current video
}
if (v.video == video) {
videoExists = true;
break;
}
}
if (videoExists) {
continue;
} else {
if(Debug.debug && Debug.periodic && Debug.videoRescan){
console.log("[PageInfo::rescan] found new video candidate:", video)
}
v = new VideoData(video, this.settings, this);
// console.log("[PageInfo::rescan] v is:", v)
// debugger;
v.initArDetection();
this.videos.push(v);
if(Debug.debug && Debug.periodic && Debug.videoRescan){
console.log("[PageInfo::rescan] — videos[] is now this:", this.videos,"\n\n\n\n\n\n\n\n")
}
}
}
this.removeDestroyed();
// console.log("Rescan complete. Total videos?", this.videos.length)
}catch(e){
console.log("rescan error:",e)
}
if(rescanReason == RescanReason.PERIODIC){
this.scheduleRescan(RescanReason.PERIODIC);
}
}
removeDestroyed(){
this.videos = this.videos.filter( vid => vid.destroyed === false);
}
scheduleRescan(rescanReason){
if(rescanReason != RescanReason.PERIODIC){
this.rescan(rescanReason);
return;
}
try{
if(this.rescanTimer){
clearTimeout(this.rescanTimer);
}
var ths = this;
this.rescanTimer = setTimeout(function(rr){
ths.rescanTimer = null;
ths.rescan(rr);
ths = null;
}, rescanReason === this.settings.active.pageInfo.timeouts.rescan, RescanReason.PERIODIC)
} catch(e) {
if(Debug.debug){
console.log("[PageInfo::scheduleRescan] scheduling rescan failed. Here's why:",e)
}
}
}
scheduleUrlCheck() {
try{
if(this.urlCheckTimer){
clearTimeout(this.urlCheckTimer);
}
var ths = this;
this.rescanTimer = setTimeout(function(){
ths.rescanTimer = null;
ths.ghettoUrlCheck();
ths = null;
}, this.settings.active.pageInfo.timeouts.urlCheck)
}catch(e){
if(Debug.debug){
console.log("[PageInfo::scheduleUrlCheck] scheduling URL check failed. Here's why:",e)
}
}
}
ghettoUrlCheck() {
if (this.lastUrl != window.location.href){
if(Debug.debug){
console.log("[PageInfo::ghettoUrlCheck] URL has changed. Triggering a rescan!");
}
this.rescan(RescanReason.URL_CHANGE);
this.lastUrl = window.location.href;
}
this.scheduleUrlCheck();
}
initArDetection(){
for(var vd of this.videos){
vd.initArDetection();
}
}
// to je treba klicat ob menjavi zavihkov
// these need to be called on tab switch
pauseProcessing(){
for(var vd of this.videos){
vd.pause();
}
}
resumeProcessing(resumeAutoar = false){
for(var vd of this.videos){
vd.resume();
if(resumeAutoar){
vd.resumeAutoAr();
}
}
}
startArDetection(){
for(var vd of this.videos){
vd.startArDetection();
}
}
stopArDetection(){
for(var vd of this.videos){
vd.stopArDetection();
}
}
setAr(ar){
if(ar !== 'auto') {
this.stopArDetection();
}
// TODO: find a way to only change aspect ratio for one video
if (ar === 'reset') {
for (var vd of this.videos) {
vd.resetAr();
}
} else {
for (var vd of this.videos) {
vd.setAr(ar)
}
}
}
setVideoFloat(videoFloat) {
for(var vd of this.videos) {
vd.setVideoFloat(videoFloat)
}
}
setPanMode(mode) {
for(var vd of this.videos) {
vd.setPanMode(mode);
}
}
restoreAr() {
for(var vd of this.videos){
vd.restoreAr()
}
}
setStretchMode(sm){
// TODO: find a way to only change aspect ratio for one video
for(var vd of this.videos){
vd.setStretchMode(sm)
}
}
setZoom(zoomLevel, no_announce) {
for(var vd of this.videos) {
vd.setZoom(zoomLevel, no_announce);
}
}
zoomStep(step){
for(var vd of this.videos){
vd.zoomStep(step);
}
}
announceZoom(scale) {
if (this.announceZoomTimeout) {
clearTimeout(this.announceZoom);
}
this.currentZoomScale = scale;
const ths = this;
this.announceZoomTimeout = setTimeout(() => ths.comms.announceZoom(scale), this.settings.active.zoom.announceDebounce);
}
requestCurrentZoom() {
this.comms.announceZoom(this.currentZoomScale);
}
}
var RescanReason = {
PERIODIC: 0,
URL_CHANGE: 1,
MANUAL: 2
}

View File

@ -1,470 +0,0 @@
if(Debug.debug)
console.log("Loading: Resizer.js");
var StretchMode = {
NO_STRETCH: 0,
BASIC: 1,
HYBRID: 2,
CONDITIONAL: 3
}
class Resizer {
constructor(videoData){
this.conf = videoData;
this.video = videoData.video;
this.settings = videoData.settings;
this.scaler = new Scaler(this.conf);
this.stretcher = new Stretcher(this.conf);
this.zoom = new Zoom(this.conf);
// load up default values
this.correctedVideoDimensions = {};
this.currentCss = {};
this.currentStyleString = "";
this.currentCssValidFor = {};
// restore watchdog. While true, applyCss() tries to re-apply new css until this value becomes false again
// value becomes false when width and height of <video> tag match with what we want to set. Only necessary when
// calling _res_restore() for some weird reason.
this.restore_wd = false;
// CSS watcher will trigger _very_ often for this many iterations
this.cssWatcherIncreasedFrequencyCounter = 0;
this.lastAr = this.settings.getDefaultAr(); // this is the aspect ratio we start with
this.videoFloat = this.settings.getDefaultVideoAlignment(); // this is initial video alignment
this.destroyed = false;
this.resizerId = (Math.random(99)*100).toFixed(0);
if (this.settings.active.pan) {
console.log("can pan:", this.settings.active.miscFullscreenSettings.mousePan.enabled, "(default:", this.settings.active.miscFullscreenSettings.mousePan.enabled, ")")
this.canPan = this.settings.active.miscFullscreenSettings.mousePan.enabled;
} else {
this.canPan = false;
}
}
start(){
if(!this.destroyed) {
this.startCssWatcher();
}
}
stop(){
this.stopCssWatcher();
}
destroy(){
if(Debug.debug || Debug.init){
console.log(`[Resizer::destroy] <rid:${this.resizerId}> received destroy command.`);
}
this.destroyed = true;
this.stopCssWatcher();
}
setAr(ar, lastAr){
if (this.destroyed) {
return;
}
this.startCssWatcher();
this.cssWatcherIncreasedFrequencyCounter = 20;
if(Debug.debug){
console.log('[Resizer::setAr] <rid:'+this.resizerId+'> trying to set ar. New ar:', ar)
}
if(lastAr) {
this.lastAr = lastAr;
} else {
if(isNaN(ar)){
this.lastAr = {type: 'legacy', ar: ar}
} else {
this.lastAr = {type: 'static', ar: ar};
}
}
if (! this.video) {
// console.log("No video detected.")
this.videoData.destroy();
}
// // pause AR on basic stretch, unpause when using other mdoes
// fir sine reason unpause doesn't unpause. investigate that later
// if (this.stretcher.mode === StretchMode.BASIC) {
// this.conf.arDetector.pause();
// } else {
// this.conf.arDetector.unpause();
// }
// do stretch thingy
if (this.stretcher.mode === StretchMode.NO_STRETCH || this.stretcher.mode === StretchMode.CONDITIONAL){
var stretchFactors = this.scaler.calculateCrop(ar);
if(! stretchFactors || stretchFactors.error){
if(Debug.debug){
console.log("[Resizer::setAr] <rid:"+this.resizerId+"> failed to set AR due to problem with calculating crop. Error:", (stretchFactors ? stretchFactors.error : stretchFactors));
}
if(stretchFactors.error === 'no_video'){
this.conf.destroy();
}
return;
}
if(this.stretcher.mode === StretchMode.CONDITIONAL){
this.stretcher.applyConditionalStretch(stretchFactors, ar);
}
} else if (this.stretcher.mode === StretchMode.HYBRID) {
var stretchFactors = this.stretcher.calculateStretch(ar);
} else if (this.stretcher.mode === StretchMode.BASIC) {
var stretchFactors = this.stretcher.calculateBasicStretch();
}
this.zoom.applyZoom(stretchFactors);
//TODO: correct these two
var translate = this.computeOffsets(stretchFactors);
this.applyCss(stretchFactors, translate);
}
resetLastAr() {
this.lastAr = {type: 'original'};
}
setLastAr(override){
this.lastAr = override;
}
getLastAr(){
return this.lastAr;
}
setStretchMode(stretchMode){
this.stretcher.mode = stretchMode;
this.restore();
}
panHandler(event) {
// console.log("this.conf.canPan:", this.conf.canPan)
if (this.canPan) {
// console.log("event?", event)
// console.log("this?", this)
if(!this.conf.player || !this.conf.player.element) {
return;
}
const player = this.conf.player.element;
const relativeX = (event.pageX - player.offsetLeft) / player.offsetWidth;
const relativeY = (event.pageY - player.offsetTop) / player.offsetHeight;
this.setPan(relativeX, relativeY);
}
}
setPan(relativeMousePosX, relativeMousePosY){
// relativeMousePos[X|Y] - on scale from 0 to 1, how close is the mouse to player edges.
// use these values: top, left: 0, bottom, right: 1
if(! this.pan){
this.pan = {};
}
this.pan.relativeOffsetX = -(relativeMousePosX * 1.1) + 0.55;
this.pan.relativeOffsetY = -(relativeMousePosY * 1.1) + 0.55;
// if(Debug.debug){
// console.log("[Resizer::setPan] relative cursor pos:", relativeMousePosX, ",",relativeMousePosY, " | new pan obj:", this.pan)
// }
this.restore();
}
setVideoFloat(videoFloat) {
this.videoFloat = videoFloat;
this.restore();
}
startCssWatcher(){
if(Debug.debug) {
console.log("[Resizer.js::startCssWatcher] starting css watcher. Is resizer destroyed?", this.destroyed);
}
if (this.destroyed) {
return;
}
// this.haltCssWatcher = false;
if(!this.cssWatcherTimer){
this.scheduleCssWatcher(1);
} else {
clearTimeout(this.cssWatcherTimer);
this.scheduleCssWatcher(1);
}
}
scheduleCssWatcher(timeout, force_reset) {
if (this.destroyed) {
return;
}
if(timeout === undefined) {
console.log("?")
this.cssCheck(); // no timeout = one-off
return;
}
if(this.cssWatcherTimeout) {
clearTimeout(this.cssWatcherTimer);
}
var ths = this;
this.cssWatcherTimer = setTimeout(function () {
ths.cssWatcherTimer = null;
try {
ths.cssCheck();
} catch (e) {
if(Debug.debug) {
console.log("[Resizer.js::scheduleCssWatcher] Css check failed. Error:", e);
}
}
}, timeout);
}
stopCssWatcher() {
if(Debug.debug) console.log("[Resizer.js] STOPPING CSS WATCHER!")
clearInterval(this.cssWatcherTimeout);
}
restore() {
if(Debug.debug){
console.log("[Resizer::restore] <rid:"+this.resizerId+"> attempting to restore aspect ratio. this & settings:", {'this': this, "settings": this.settings} );
}
// this is true until we verify that css has actually been applied
this.restore_wd = true;
if(this.lastAr.type === 'original'){
this.setAr('reset');
}
else {
this.setAr(this.lastAr.ar, this.lastAr)
}
}
reset(){
this.setStretchMode(StretchMode.NO_STRETCH);
this.zoom.setZoom(1);
this.resetPan();
this.setAr('reset');
}
setPanMode(mode) {
if (mode === 'enable') {
this.canPan = true;
} else if (mode === 'disable') {
this.canPan = false;
} else if (mode === 'toggle') {
this.canPan = !this.canPan;
}
}
resetPan(){
this.pan = undefined;
}
setZoom(zoomLevel, no_announce) {
this.zoom.setZoom(zoomLevel, no_announce);
}
zoomStep(step){
this.zoom.zoomStep(step);
}
resetZoom(){
this.zoom.setZoom(1);
this.restore();
}
resetCrop(){
this.setAr('reset');
}
resetStretch(){
this.stretcher.mode = StretchMode.NO_STRETCH;
this.restore();
}
// mostly internal stuff
computeOffsets(stretchFactors){
if (Debug.debug) {
console.log("[Resizer::_res_computeOffsets] <rid:"+this.resizerId+"> video will be aligned to ", this.settings.active.miscFullscreenSettings.videoFloat);
}
var actualWidth = this.conf.video.offsetWidth * stretchFactors.xFactor;
var actualHeight = this.conf.video.offsetHeight * stretchFactors.yFactor;
var wdiff = actualWidth - this.conf.player.dimensions.width;
var hdiff = actualHeight - this.conf.player.dimensions.height;
var translate = {x: 0, y: 0};
if (this.pan) {
// don't offset when video is smaller than player
if(wdiff < 0 && hdiff < 0) {
return translate;
}
translate.x = wdiff * this.pan.relativeOffsetX / this.zoom.scale;
translate.y = hdiff * this.pan.relativeOffsetY / this.zoom.scale;
} else {
if (this.videoFloat == "left") {
translate.x = wdiff * 0.5;
}
else if (this.videoFloat == "right") {
translate.x = wdiff * -0.5;
}
}
if(Debug.debug) {
console.log("[Resizer::_res_computeOffsets] <rid:"+this.resizerId+"> calculated offsets:", translate);
}
return translate;
}
applyCss(stretchFactors, translate){
if (! this.video) {
if(Debug.debug) {
console.log("[Resizer::_res_applyCss] <rid:"+this.resizerId+"> Video went missing, doing nothing.");
}
this.conf.destroy();
return;
}
// save stuff for quick tests (before we turn numbers into css values):
this.currentVideoSettings = {
validFor: this.conf.player.dimensions,
// videoWidth: dimensions.width,
// videoHeight: dimensions.height
}
var styleArrayStr = this.video.getAttribute('style');
if (styleArrayStr) {
var styleArray = styleArrayStr.split(";");
for(var i in styleArray){
styleArray[i] = styleArray[i].trim();
if (styleArray[i].startsWith("transform:")){
delete styleArray[i];
}
}
}
else{
var styleArray = [];
}
// add remaining elements
if(stretchFactors){
styleArray.push(`transform: scale(${stretchFactors.xFactor}, ${stretchFactors.yFactor}) translate(${translate.x}px, ${translate.y}px)`);
}
// build style string back
var styleString = "";
for(var i in styleArray)
if(styleArray[i])
styleString += styleArray[i] + "; ";
this.setStyleString(styleString);
}
setStyleString (styleString, count = 0) {
this.video.setAttribute("style", styleString);
this.currentStyleString = this.video.getAttribute('style');
this.currentCssValidFor = this.conf.player.dimensions;
if(this.restore_wd){
if(! this.video){
if(Debug.debug)
console.log("[Resizer::_res_setStyleString] <rid:"+this.resizerId+"> Video element went missing, nothing to do here.")
return;
}
// if(
// styleString.indexOf("width: " + this.video.style.width) == -1 ||
// styleString.indexOf("height: " + this.video.style.height) == -1) {
// // css ni nastavljen?
// // css not set?
// if(Debug.debug)
// console.log("[Resizer::_res_setStyleString] Style string not set ???");
// if(count < settings.active.resizer.setStyleString.maxRetries){
// setTimeout( this.setStyleString, settings.active.resizer.setStyleString.retryTimeout, count + 1);
// }
// else if(Debug.debug){
// console.log("[Resizer::_res_setStyleString] we give up. css string won't be set");
// }
// }
// else{
this.restore_wd = false;
// }
}
else{
if(Debug.debug)
console.log("[Resizer::_res_setStyleString] <rid:"+this.resizerId+"> css applied. Style string:", styleString);
}
}
cssCheck(){
// this means we haven't set our CSS yet, or that we changed video.
// if(! this.currentCss.tranform) {
// this.scheduleCssWatcher(200);
// return;
// }
// this means video went missing. videoData will be re-initialized when the next video is found
if(! this.video){
if(Debug.debug) {
console.log("[Resizer::cssCheck] <rid:"+this.resizerId+"> no video detecting, issuing destroy command");
}
this.conf.destroy();
return;
}
if(this.destroyed) {
if(Debug.debug) {
console.log("[Resizer::cssCheck] <rid:"+this.resizerId+"> destroyed flag is set, we shouldnt be running");
}
this.stopCssWatcher();
return;
}
var styleString = this.video.getAttribute('style');
// first, a quick test:
// if (this.currentVideoSettings.validFor == this.conf.player.dimensions ){
if (this.currentStyleString !== styleString){
this.restore();
this.scheduleCssWatcher(10);
return;
}
if (this.cssWatcherIncreasedFrequencyCounter > 0) {
--this.cssWatcherIncreasedFrequencyCounter;
this.scheduleCssWatcher(20);
} else {
this.scheduleCssWatcher(1000);
}
}
}

View File

@ -1,84 +0,0 @@
var BgVars = {
arIsActive: true,
hasVideos: false,
currentSite: ""
}
class UWServer {
constructor() {
this.ports = [];
this.arIsActive = true;
this.hasVideos = false;
this.currentSite = "";
this.setup();
}
async setup() {
this.settings = new Settings();
await this.settings.init();
this.comms = new CommsServer(this);
var ths = this;
if(BrowserDetect.firefox) {
browser.tabs.onActivated.addListener(function(m) {ths.onTabSwitched(m)});
} else if (BrowserDetect.chrome) {
chrome.tabs.onActivated.addListener(function(m) {ths.onTabSwitched(m)});
}
}
async _promisifyTabsGet(browserObj, tabId){
return new Promise( (resolve, reject) => {
browserObj.tabs.get(tabId, (tab) => resolve(tab));
});
}
extractHostname(url){
var hostname;
// 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;
if(Debug.debug)
console.log("[uw-bg::onTabSwitched] TAB CHANGED, GETTING INFO FROM MAIN TAB");
try {
var tabId = activeInfo.tabId; // just for readability
var tab;
if (BrowserDetect.firefox) {
var tab = await browser.tabs.get(tabId);
} else if (BrowserDetect.chrome) {
var tab = await this._promisifyTabsGet(chrome, tabId);
}
this.currentSite = this.extractHostname(tab.url);
} catch(e) {
console.log(e);
}
if(Debug.debug) {
console.log("TAB SWITCHED!", this.currentSite)
}
//TODO: change extension icon based on whether there's any videos on current page
}
}
var server = new UWServer();

View File

@ -1,95 +0,0 @@
{
"manifest_version": 2,
"name": "Ultrawidify",
"version": "3.2.2",
"applications": {
"gecko": {
"id": "{cf02b1a7-a01a-4e37-a609-516a283f1ed3}"
}
},
"icons": {
"32":"res/icons/uw-32.png",
"64":"res/icons/uw-64.png"
},
"description": "Aspect ratio fixer for youtube that works around some people's disability to properly encode 21:9 (and sometimes, 16:9) videos.",
"content_scripts": [{
"matches": ["*://*/*"],
"js": [
"js/conf/Debug.js",
"js/lib/BrowserDetect.js",
"js/conf/ExtensionConf.js",
"js/lib/ObjectCopy.js",
"js/lib/Settings.js",
"js/lib/Comms.js",
"js/lib/EdgeDetect.js",
"js/lib/GuardLine.js",
"js/modules/PageInfo.js",
"js/modules/DebugCanvas.js",
"js/modules/ArDetect.js",
"js/modules/Zoom.js",
"js/modules/Scaler.js",
"js/modules/Stretcher.js",
"js/modules/Resizer.js",
"js/lib/PlayerData.js",
"js/lib/VideoData.js",
"js/conf/Keybinds.js",
"js/uw.js" ],
"all_frames": true
}],
"background": {
"scripts": [
"js/conf/Debug.js",
"js/lib/BrowserDetect.js",
"js/conf/ExtensionConf.js",
"js/lib/Comms.js",
"js/lib/ObjectCopy.js",
"js/lib/Settings.js",
"js/conf/Keybinds.js",
"js/uw-bg.js"
]
},
"permissions": [
"tabs", "storage", "activeTab", "<all_urls>", "webNavigation"
],
"browser_action": {
"default_icon": "res/icons/uw-32.png",
"default_popup": "res/popup/popup.html",
"default_title": "Ultrawidify"
},
"web_accessible_resources": [
"js/*",
"res/fonts/*",
"res/css/*",
"res/img/ytplayer-icons/zoom.png",
"res/img/ytplayer-icons/unzoom.png",
"res/img/ytplayer-icons/fitw.png",
"res/img/ytplayer-icons/fith.png",
"res/img/ytplayer-icons/reset.png",
"res/img/ytplayer-icons/settings.png",
"res/img/settings/about-bg.png"
]
}

51
package.json Normal file
View File

@ -0,0 +1,51 @@
{
"name": "ultravidify-git",
"version": "4.0.0-a1",
"description": "Aspect ratio fixer for youtube that works around some people's disability to properly encode 21:9 (and sometimes, 16:9) videos.",
"author": "Tamius Han <tamius.han@gmail.com>",
"scripts": {
"build": "cross-env NODE_ENV=production BROWSER=firefox webpack --hide-modules",
"build-chrome": "cross-env NODE_ENV=production BROWSER=chrome webpack --hide-modules",
"build-edge": "cross-env NODE_ENV=production BROWSER=edge webpack --hide-modules",
"build:dev": "cross-env NODE_ENV=development BROWSER=firefox webpack --hide-modules",
"build-chrome:dev": "cross-env NODE_ENV=development BROWSER=chrome webpack --hide-modules",
"build-edge:dev": "cross-env NODE_ENV=development BROWSER=edge webpack --hide-modules",
"build-zip": "node scripts/build-zip.js",
"watch": "npm run build -- --watch",
"watch-chrome": "npm run build-chrome-- --watch",
"watch-edge": "npm run build-edge -- --watch",
"watch:dev": "cross-env HMR=true npm run build:dev -- --watch",
"watch-chrome:dev": "cross-env HMR=true npm run build-chrome:dev -- --watch",
"watch-edge:dev": "cross-env HMR=true npm run build-edge:dev -- --watch"
},
"dependencies": {
"@types/core-js": "^2.5.0",
"@types/es6-promise": "^3.3.0",
"fs-extra": "^7.0.1",
"vue": "^2.5.17",
"vuex": "^3.0.1",
"vuex-webextensions": "^1.2.6"
},
"devDependencies": {
"@babel/core": "^7.1.2",
"@babel/plugin-proposal-optional-chaining": "^7.0.0",
"@babel/preset-env": "^7.1.0",
"archiver": "^3.0.0",
"babel-loader": "^8.0.2",
"copy-webpack-plugin": "^4.5.3",
"cross-env": "^5.2.0",
"css-loader": "^0.28.11",
"ejs": "^2.6.1",
"file-loader": "^1.1.11",
"mini-css-extract-plugin": "^0.4.4",
"node-sass": "^4.9.3",
"sass-loader": "^7.1.0",
"vue-loader": "^15.4.2",
"vue-template-compiler": "^2.5.17",
"web-ext-types": "^2.1.0",
"webpack": "^4.20.2",
"webpack-chrome-extension-reloader": "^0.8.3",
"webpack-cli": "^3.1.2",
"webpack-shell-plugin": "^0.5.0"
}
}

Binary file not shown.

View File

@ -1,214 +0,0 @@
html, body {
color: #f8f8f8;
background-color: #1f1f1f;
font-family: "overpass";
font-size: 1em;
user-select: none;
-moz-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
}
/* STANDARD WIDTHS AND HEIGHTS */
.w100 {
width: 100%;
}
.w24 {
width: 100px;
}
.m-t-0-33em {
margin-top: 0.33em;
}
.x-pad-1em {
padding-left: 1em;
padding-right: 1em;
}
/* ELEMENT POSITIONING */
.row {
display: block;
margin-top: 20px;
margin-bottom: 10px;
}
.button-row {
display: block;
margin-top: 5px;
margin-bottom: 10px;
}
.float-left {
float: left;
}
.float-right {
float: right;
}
.inline-block {
display: inline-block;
}
.block {
display: block;
}
.hidden {
display: none !important;
}
.hide {
display: none !important;
}
/* TEXT FORMATTING (no colors) */
small {
font-size: 0.75em;
font-weight: 200;
}
.small {
font-size: 0.75em;
font-weight: 200;
}
.medium-small {
font-size: 0.85em;
font-weight: 200;
}
.smallcaps{
font-variant: small-caps;
}
.center{
text-align: center;
width: 100%;
}
/* COLORS */
a {
color: #af7f37;
}
a:hover {
color: #c0924e;
}
.color_warn {
color: #d6ba4a;
}
strike {
opacity: 0.5;
}
.darker {
color: #666;
}
.label{
font-size: 1.1em;
font-weight: 600;
color: #ffe;
}
.selected{
color: #ffddaa;
background-color: #433221;
}
.disabled {
pointer-events: none;
/* color: #666; */
filter: contrast(50%) brightness(40%) grayscale(100%);
}
.disabled-button {
color: #666 !important;
cursor: not-allowed !important;
}
.disabled-button:hover {
color: #777 !important;
background-color: #222 !important;
}
/* BUTTONS AND INPUTS */
input {
border: 1px solid #322;
padding: 0.5em;
padding-top: 0.5em;
padding-bottom: 0.5em;
}
.invalid-input {
border: 1px solid #720 !important;
background-color: #410 !important;
}
.button {
display: inline-block;
padding-top: 8px;
padding-bottom: 3px;
padding-left: 20px;
padding-right: 20px;
border: 1px solid #444;
margin-top: 3px;
margin-bottom: 3px;
color: #dbb;
text-align: center;
}
.button:hover {
color: #fff;
background-color: #433221;
}
/** misc **/
.warning {
color: #d6ba4a;
padding-left: 35px;
float: right;
}
.warning::before {
content: "⚠ ";
display: inline-block;
font-weight: bold;
font-size: 2.5em;
margin-left: -35px;
padding-right: 10px;
}
.experimental::after {
content: "Experimental";
background-color: #ffde12;
color: #1f1f1f;
display: inline-block;
font-size: .75em;
font-variant: small-caps;
padding-left: 5px;
padding-right: 5px;
margin-left: 10px;
}

View File

@ -1,59 +0,0 @@
html, body {
width: 780px !important;
max-width: 800px !important;
padding: 0px;
margin: 0px;
}
.header {
background-color: #7f1416;
color: #fff;
margin: 0px;
margin-top: 0px;
padding-top: 8px;
padding-left: 15px;
padding-bottom: 1px;
font-size: 2.7em;
}
.menu-item-inline-desc{
font-size: 0.60em;
font-weight: 300;
font-variant: normal;
}
.left-side {
display: inline-block;
width: 39%;
float: left;
font-size: 1.6em;
}
.right-side {
display: inline-block;
width: 60%;
float: right;
}
.menu-item {
padding-left: 15px;
padding-top: 5px;
padding-bottom: 5px;
font-variant: small-caps;
}
.suboption {
display: block;
padding-left: 15px;
padding-right: 15px;
padding-top: 5px;
padding-bottom: 20px;
min-height: 250px;
}
#no-videos-display {
height: 100%;
padding-top: 50px;
/* text-align: center; */
}

View File

@ -1,844 +0,0 @@
if(Debug.debug)
console.log("[popup.js] loading popup script!");
document.getElementById("uw-version").textContent = browser.runtime.getManifest().version;
var Menu = {};
// Menu.noVideo = document.getElementById("no-videos-display");
Menu.extensionSettings = document.getElementById('_menu_settings_ext');
Menu.siteSettings = document.getElementById('_menu_settings_site');
Menu.videoSettings = document.getElementById('_menu_settings_video');
Menu.about = document.getElementById('_menu_about')
var MenuTab = {};
MenuTab.extensionSettings = document.getElementById('_menu_tab_settings_ext');
MenuTab.siteSettings = document.getElementById('_menu_tab_settings_site');
MenuTab.videoSettings = document.getElementById('_menu_tab_settings_video');
MenuTab.about = document.getElementById('_menu_tab_about')
//#region ExtPanel
var ExtPanel = {};
ExtPanel.extOptions = {};
ExtPanel.extOptions.blacklist = document.getElementById("_ext_global_options_blacklist");
ExtPanel.extOptions.whitelist = document.getElementById("_ext_global_options_whitelist");
ExtPanel.extOptions.disabled = document.getElementById("_ext_global_options_disabled");
ExtPanel.arOptions = {};
ExtPanel.arOptions.blacklist = document.getElementById("_ar_global_options_blacklist");
ExtPanel.arOptions.whitelist = document.getElementById("_ar_global_options_whitelist");
ExtPanel.arOptions.disabled = document.getElementById("_ar_global_options_disabled");
ExtPanel.alignment = {};
ExtPanel.alignment.left = document.getElementById("_align_ext_left");
ExtPanel.alignment.center = document.getElementById("_align_ext_center");
ExtPanel.alignment.right = document.getElementById("_align_ext_right");
ExtPanel.stretch = {};
ExtPanel.stretch['0'] = document.getElementById("_stretch_global_none");
ExtPanel.stretch['1'] = document.getElementById("_stretch_global_basic");
ExtPanel.stretch['2'] = document.getElementById("_stretch_global_hybrid");
ExtPanel.stretch['3'] = document.getElementById("_stretch_global_conditional");
//#endregion
//#region SitePanel
var SitePanel = {};
SitePanel.extOptions = {};
SitePanel.extOptions.enabled = document.getElementById("_ext_site_options_whitelist");
SitePanel.extOptions.default = document.getElementById("_ext_site_options_default");
SitePanel.extOptions.disabled = document.getElementById("_ext_site_options_blacklist");
SitePanel.arOptions = {};
SitePanel.arOptions.disabled = document.getElementById("_ar_site_options_disabled");
SitePanel.arOptions.enabled = document.getElementById("_ar_site_options_enabled");
SitePanel.arOptions.default = document.getElementById("_ar_site_options_default");
SitePanel.alignment = {};
SitePanel.alignment.left = document.getElementById("_align_site_left");
SitePanel.alignment.center = document.getElementById("_align_site_center");
SitePanel.alignment.right = document.getElementById("_align_site_right");
SitePanel.alignment.default = document.getElementById("_align_site_default");
SitePanel.stretch = {};
SitePanel.stretch['-1'] = document.getElementById("_stretch_site_default")
SitePanel.stretch['0'] = document.getElementById("_stretch_site_none")
SitePanel.stretch['1'] = document.getElementById("_stretch_site_basic")
SitePanel.stretch['2'] = document.getElementById("_stretch_site_hybrid")
SitePanel.stretch['3'] = document.getElementById("_stretch_site_conditional")
//#endregion
//#region VideoPanel
var VideoPanel = {};
VideoPanel.alignment = {};
VideoPanel.alignment.left = document.getElementById("_align_video_left");
VideoPanel.alignment.center = document.getElementById("_align_video_center");
VideoPanel.alignment.right = document.getElementById("_align_video_right");
// labels on buttons
VideoPanel.buttonLabels = {};
VideoPanel.buttonLabels.crop = {};
VideoPanel.buttonLabels.crop['auto-ar'] = document.getElementById("_b_changeAr_auto-ar_key");
VideoPanel.buttonLabels.crop.reset = document.getElementById("_b_changeAr_reset_key");
VideoPanel.buttonLabels.crop['219'] = document.getElementById("_b_changeAr_219_key");
VideoPanel.buttonLabels.crop['189'] = document.getElementById("_b_changeAr_189_key");
VideoPanel.buttonLabels.crop['169'] = document.getElementById("_b_changeAr_169_key");
VideoPanel.buttonLabels.crop.custom = document.getElementById("_b_changeAr_custom_key");
VideoPanel.buttonLabels.zoom = {};
// buttons: for toggle, select
VideoPanel.buttons = {};
VideoPanel.buttons.zoom = {};
VideoPanel.buttons.zoom.showShortcuts = document.getElementById("_zoom_b_show_shortcuts");
VideoPanel.buttons.zoom.hideShortcuts = document.getElementById("_zoom_b_hide_shortcuts");
VideoPanel.buttons.changeAr = {};
VideoPanel.buttons.changeAr.showCustomAr = document.getElementById("_changeAr_b_show_customAr");
VideoPanel.buttons.changeAr.hideCustomAr = document.getElementById("_changeAr_b_hide_customAr");
// inputs (getting values)
VideoPanel.inputs = {};
VideoPanel.inputs.customCrop = document.getElementById("_input_custom_ar");
VideoPanel.inputs.zoomSlider = document.getElementById("_input_zoom_slider");
VideoPanel.inputs.allowPan = document.getElementById("_input_zoom_site_allow_pan");
// various labels
VideoPanel.labels = {};
VideoPanel.labels.zoomLevel = document.getElementById("_label_zoom_level");
// misc stuff
VideoPanel.misc = {};
VideoPanel.misc.zoomShortcuts = document.getElementById("_zoom_shortcuts");
VideoPanel.misc.customArChanger = document.getElementById("_changeAr_customAr");
//#endregion
var selectedMenu = "";
var hasVideos = false;
var zoom_videoScale = 1;
var _config;
var _changeAr_button_shortcuts = { "autoar":"none", "reset":"none", "219":"none", "189":"none", "169":"none", "custom":"none" }
var comms = new Comms();
var settings = new Settings(undefined, () => updateConfig());
var site = undefined;
var port = browser.runtime.connect({name: 'popup-port'});
port.onMessage.addListener( (m,p) => processReceivedMessage(m,p));
async function processReceivedMessage(message, port){
if (Debug.debug) {
console.log("[popup.js] received message", message)
}
if(message.cmd === 'set-current-site'){
if (site !== message.site) {
port.postMessage({cmd: 'get-current-zoom'});
}
site = message.site;
loadConfig(message.site);
} else if (message.cmd === 'set-current-zoom') {
setCurrentZoom(message.zoom);
}
}
async function updateConfig() {
if (Debug.debug) {
console.log("[popup.js] settings changed. updating popup if site exists. Site:", site);
}
if (site) {
loadConfig(site);
}
}
async function setCurrentZoom(scale) {
zoom_videoScale = scale;
if(Debug.debug) {
console.log("[popup.js::setCurrentZoom] we're setting zoom:", zoom_videoScale);
}
VideoPanel.inputs.zoomSlider.value = Math.log2(zoom_videoScale);
VideoPanel.labels.zoomLevel.textContent = (zoom_videoScale * 100).toFixed();
}
function hideWarning(warn){
document.getElementById(warn).classList.add("hidden");
}
function stringToKeyCombo(key_in){
var keys_in = key_in.split("_");
var keys_out = "";
for(key of keys_in){
if(key == "ctrlKey")
keys_out += "ctrl + ";
else if(key == "shiftKey")
keys_out += "shift + ";
else if(key == "altKey")
keys_out += "alt + ";
else
keys_out += key;
}
return keys_out;
}
function configurePopupTabs(site) {
// Determine which tabs can we touch.
// If extension is disabled, we can't touch 'site settings' and 'video settings'
// If extension is enabled, but site is disabled, we can't touch 'video settings'
var extensionEnabled = settings.extensionEnabled();
var extensionEnabledForSite = settings.extensionEnabledForSite(site);
MenuTab.siteSettings.classList.add('disabled');
if (extensionEnabledForSite) {
MenuTab.videoSettings.classList.remove('disabled');
}
if (extensionEnabled) {
MenuTab.videoSettings.classList.remove('disabled');
}
// we assume that these two can be shown. If extension or site are disabled, we'll
// add 'disabled' class later down the line:
document.getElementById("_site_only_when_site_enabled").classList.remove("disabled");
document.getElementById("_ext_only_when_ext_enabled").classList.remove("disabled");
if (! extensionEnabledForSite) {
MenuTab.videoSettings.classList.add('disabled');
// also disable extra settings for site
document.getElementById("_site_only_when_site_enabled").classList.add("disabled");
if (! extensionEnabled) {
MenuTab.siteSettings.classList.add('disabled');
// also disable extra settings for extension
document.getElementById("_ext_only_when_ext_enabled").classList.add("disabled");
if (!selectedMenu) {
openMenu('extensionSettings');
}
} else {
MenuTab.siteSettings.classList.remove('disabled');
if (!selectedMenu) {
openMenu('siteSettings');
}
}
} else {
MenuTab.videoSettings.classList.remove('disabled');
MenuTab.siteSettings.classList.remove('disabled');
// if popup isn't being opened for the first time, there's no reason to switch
// we're already in this tab
if (!selectedMenu) {
openMenu('videoSettings');
}
}
}
function configureGlobalTab() {
if (Debug.debug) {
console.log("[popup.js] Configuring global tab (ExtPanel).",
"\nextension mode: ", settings.active.extensionMode,
"\narDetect mode: ", settings.active.arDetect.mode,
"\nvideo float mode:", settings.active.miscFullscreenSettings.videoFloat,
"\nstretch mode: ", settings.active.stretch.initialMode,
"\n..")
}
for(var button in ExtPanel.extOptions) {
ExtPanel.extOptions[button].classList.remove("selected");
}
for(var button in ExtPanel.arOptions) {
ExtPanel.arOptions[button].classList.remove("selected");
}
for(var button in ExtPanel.alignment) {
ExtPanel.alignment[button].classList.remove("selected");
}
for(var button in ExtPanel.stretch) {
ExtPanel.stretch[button].classList.remove("selected");
}
ExtPanel.extOptions[settings.active.extensionMode].classList.add("selected");
ExtPanel.arOptions[settings.active.arDetect.mode].classList.add("selected");
ExtPanel.alignment[settings.active.miscFullscreenSettings.videoFloat].classList.add("selected");
ExtPanel.stretch[settings.active.stretch.initialMode].classList.add("selected");
}
function configureSitesTab(site) {
if (Debug.debug) {
console.log("[popup.js] Configuring sites tab (SitePanel).",
"\nsite: ", site,
"\nextension mode: ", settings.active.sites[site] ? settings.active.sites[site].status : 'no site-special settings for this site',
"\narDetect mode: ", settings.active.sites[site] ? settings.active.sites[site].arStatus : 'no site-special settings for this site',
"\nvideo float mode:", settings.active.sites[site] ? settings.active.sites[site].videoFloat : 'no site-special settings for this site',
"\ndefault ar: ", settings.active.sites[site] ? settings.active.sites[site].ar : 'no site-special settings for this site',
"\nstretch mode: ", settings.active.sites[site] ? settings.active.sites[site].stretch : 'no site-special settings for this site',
"\n...")
}
for(const button in SitePanel.extOptions) {
SitePanel.extOptions[button].classList.remove("selected");
}
for(const button in SitePanel.arOptions) {
SitePanel.arOptions[button].classList.remove("selected");
}
for(const button in SitePanel.alignment) {
SitePanel.alignment[button].classList.remove("selected");
}
for(const button in SitePanel.stretch) {
SitePanel.stretch[button].classList.remove("selected");
}
if (settings.active.sites[site] && settings.active.sites[site]) {
console.log("settings for", site, "exist!")
SitePanel.extOptions[settings.active.sites[site].status].classList.add("selected");
SitePanel.arOptions[settings.active.sites[site].arStatus].classList.add("selected");
} else {
SitePanel.extOptions.default.classList.add("selected");
SitePanel.arOptions.default.classList.add("selected");
}
// optional settings:
if (settings.active.sites[site] && settings.active.sites[site].videoAlignment) {
SitePanel.alignment[settings.active.sites[site].videoAlignment].classList.add("selected");
} else {
SitePanel.alignment.default.classList.add('selected');
}
if(settings.active.sites[site] && settings.active.sites[site].stretch !== undefined) { // can be 0
SitePanel.stretch[settings.active.sites[site].stretch].classList.add("selected");
} else {
SitePanel.stretch['-1'].classList.add("selected");
}
}
function configureVideoTab() {
// process keyboard shortcuts for crop settings
if(settings.active.keyboard.shortcuts){
for(var key in settings.active.keyboard.shortcuts){
var shortcut = settings.active.keyboard.shortcuts[key];
var keypress = stringToKeyCombo(key);
try{
if(shortcut.action == "crop"){
if (key == 'q') {
_changeAr_button_shortcuts["custom"] = keypress;
}
else if(shortcut.arg == 2.0){
_changeAr_button_shortcuts["189"] = keypress;
}
else if(shortcut.arg == 2.39){
_changeAr_button_shortcuts["219"] = keypress;
}
else if(shortcut.arg == 1.78){
_changeAr_button_shortcuts["169"] = keypress;
}
else if(shortcut.arg == "fitw") {
_changeAr_button_shortcuts["fitw"] = keypress;
}
else if(shortcut.arg == "fith") {
_changeAr_button_shortcuts["fith"] = keypress;
}
else if(shortcut.arg == "reset") {
_changeAr_button_shortcuts["reset"] = keypress;
}
}
else if(shortcut.action == "auto-ar") {
_changeAr_button_shortcuts["auto-ar"] = keypress;
}
}
catch(Ex){
//do nothing if key doesn't exist
}
}
// fill in custom aspect ratio
if (settings.active.keyboard.shortcuts.q) {
document.getElementById("_input_custom_ar").value = settings.active.keyboard.shortcuts.q.arg;
}
for(var key in _changeAr_button_shortcuts){
try{
document.getElementById(`_b_changeAr_${key}_key`).textContent = `(${_changeAr_button_shortcuts[key]})`;
}
catch(ex){
}
}
}
// todo: get min, max from settings
VideoPanel.inputs.zoomSlider.min = Math.log2(0.5);
VideoPanel.inputs.zoomSlider.max = Math.log2(8);
VideoPanel.inputs.zoomSlider.value = Math.log2(zoom_videoScale);
VideoPanel.inputs.zoomSlider.addEventListener('input', (event) => {
var newZoom = Math.pow(2, VideoPanel.inputs.zoomSlider.value);
// save value so it doesn't get reset next time the popup updates
zoom_videoScale = newZoom;
// update zoom% label
VideoPanel.labels.zoomLevel.textContent = (newZoom * 100).toFixed();
// send the command to bg script
var command = {
cmd: 'set-zoom',
zoom: newZoom
};
port.postMessage(command);
});
}
async function loadConfig(site){
console.log("NEW CONFIG!")
if (Debug.debug) {
console.log("\n\n-------------------------------------\n[popup.js::loadConfig] loading config. conf object:", settings.active);
}
document.getElementById("_menu_tab_settings_site_site_label").textContent = site;
configurePopupTabs(site);
configureGlobalTab();
configureSitesTab(site);
configureVideoTab();
if (Debug.debug) {
console.log("[popup.js::loadConfig] config loaded\n-----------------------\n\n");
}
}
async function getSite(){
if (Debug.debug) {
console.log("[popup.js] requesting current site");
}
port.postMessage({cmd: 'get-current-site'});
}
function openMenu(menu){
if(Debug.debug){
console.log("[popup.js::openMenu] trying to open menu", menu, "\n element: ", Menu[menu]);
}
for(var m in Menu){
if(Menu[m])
Menu[m].classList.add("hidden");
}
for(var m in MenuTab){
if(MenuTab[m])
MenuTab[m].classList.remove("selected");
}
Menu[menu].classList.remove("hidden");
MenuTab[menu].classList.add("selected");
selectedMenu = menu;
}
function getCustomAspectRatio() {
var textBox_value = document.getElementById("_input_custom_ar").value.trim();
// validate value - this spaghett will match the following stuff
// [int]/[int]
// 1:[float]
// [float]
if (! /(^[0-9]+\/[0-9]+$|^(1:)?[0-9]+\.?[0-9]*$)/.test(textBox_value)) {
return false; // validation failed!
}
if (! isNaN(parseFloat(textBox_value))) {
return parseFloat(textBox_value);
}
if (/\//.test(textBox_value)) {
const vars = textBox_value.split('/');
return parseInt(vars[0])/parseInt(vars[1]); // non-ints shouldn't make it past regex
}
if (/:/.test(textBox_value)) {
const vars = textBox_value.split(':');
return parseFloat(vars[1]);
}
// we should never come this far.
// If we do, then there's something wrong with the input and our regex
return false;
}
function validateCustomAr(){
const valid = getCustomAspectRatio() !== false;
const inputField = document.getElementById("_input_custom_ar");
const valueSaveButton = document.getElementById("_b_changeAr_save_custom_ar");
if (valid) {
inputField.classList.remove("invalid-input");
valueSaveButton.classList.remove("disabled-button");
} else {
inputField.classList.add("invalid-input");
valueSaveButton.classList.add("disabled-button");
}
}
function validateAutoArTimeout(){
const inputField = document.getElementById("_input_autoAr_timer");
const valueSaveButton = document.getElementById("_b_autoar_save_autoar_timer");
if (! isNaN(parseInt(inputField.value.trim().value()))) {
inputField.classList.remove("invalid-input");
valueSaveButton.classList.remove("disabled-button");
} else {
inputField.classList.add("invalid-input");
valueSaveButton.classList.add("disabled-button");
}
}
document.addEventListener("click", (e) => {
function getcmd(e){
var command = {};
command.sender = "popup";
command.receiver = "uwbg";
if(e.target.classList.contains("disabled"))
return;
if(e.target.classList.contains("menu-item")){
if(Debug.debug) {
console.log("[popup.js::eventListener] clicked on a tab. Class list:", e.target.classList);
}
if(e.target.classList.contains("_menu_tab_settings_ext")){
openMenu("extensionSettings");
} else if(e.target.classList.contains("_menu_tab_settings_site")){
openMenu("siteSettings");
} else if(e.target.classList.contains("_menu_tab_settings_video")){
openMenu("videoSettings");
} else if(e.target.classList.contains("_menu_tab_about")){
openMenu("about");
}
// don't send commands
return;
}
if(e.target.classList.contains("_ext")) {
var command = {};
if(e.target.classList.contains("_ext_global_options")){
if (e.target.classList.contains("_blacklist")) {
settings.active.extensionMode = "blacklist";
} else if (e.target.classList.contains("_whitelist")) {
settings.active.extensionMode = "whitelist";
} else {
settings.active.extensionMode = "disabled";
}
settings.save();
return;
} else if (e.target.classList.contains("_ext_site_options")) {
var mode;
if(e.target.classList.contains("_blacklist")){
mode = "disabled";
} else if(e.target.classList.contains("_whitelist")) {
mode = "enabled";
} else {
mode = "default";
}
if(settings.active.sites[site]) {
settings.active.sites[site].status = mode;
settings.active.sites[site].statusEmbedded = mode;
} else {
settings.active.sites[site] = {
status: mode,
statusEmbedded: mode,
arStatus: 'default',
type: 'user-defined'
}
}
settings.save();
return;
}
}
if(e.target.classList.contains("_changeAr")){
if(e.target.classList.contains("_ar_auto")){
command.cmd = "autoar-start";
command.enabled = true;
return command;
}
if(e.target.classList.contains("_ar_reset")){
command.cmd = "set-ar";
command.ratio = "reset";
return command;
}
if(e.target.classList.contains("_ar_fitw")){
command.cmd = "set-ar";
command.ratio = "fitw";
return command;
}
if(e.target.classList.contains("_ar_fitw")){
command.cmd = "set-ar";
command.ratio = "fith";
return command;
}
if(e.target.classList.contains("_ar_219")){
command.cmd = "set-ar";
command.ratio = 2.39;
return command;
}
if(e.target.classList.contains("_ar_189")){
command.cmd = "set-ar";
command.ratio = 2.0;
return command;
}
if(e.target.classList.contains("_ar_169")){
command.cmd = "set-ar";
command.ratio = 1.78;
return command;
}
if(e.target.classList.contains("_ar_1610")){
command.cmd = "set-ar";
command.ratio = 1.6;
return command;
}
if(e.target.classList.contains("_ar_custom")){
ratio = getCustomAspectRatio();
command.cmd = "set-ar";
command.ratio = ratio;
return ratio !== false ? command : null;
}
if(e.target.classList.contains("_ar_save_custom_ar")){
ratio = getCustomAspectRatio();
command.cmd = "set-custom-ar";
command.ratio = ratio;
return ratio !== false ? command : null; // this validates input
}
}
if(e.target.classList.contains("_stretch")){
// stretch, global
if (e.target.classList.contains("_ar_stretch_global")) {
if (e.target.classList.contains("_none")) {
settings.active.stretch.initialMode = 0;
} else if (e.target.classList.contains("_basic")) {
settings.active.stretch.initialMode = 1;
} else if (e.target.classList.contains("_hybrid")) {
settings.active.stretch.initialMode = 2;
} else if (e.target.classList.contains("_conditional")) {
settings.active.stretch.initialMode = 3;
}
settings.save();
return;
}
// stretch, site
if (e.target.classList.contains("_ar_stretch_site")) {
if (e.target.classList.contains("_none")) {
settings.active.sites[site].stretch = 0;
} else if (e.target.classList.contains("_basic")) {
settings.active.sites[site].stretch = 1;
} else if (e.target.classList.contains("_hybrid")) {
settings.active.sites[site].stretch = 2;
} else if (e.target.classList.contains("_conditional")) {
settings.active.sites[site].stretch = 3;
} else {
delete(settings.active.sites[site].stretch);
}
settings.save();
return;
}
if(e.target.classList.contains("_ar_stretch_none")) {
command.cmd = "set-stretch";
command.mode = "NO_STRETCH";
} else if(e.target.classList.contains("_ar_stretch_basic")) {
command.cmd = "set-stretch";
command.mode = "BASIC";
} else if(e.target.classList.contains("_ar_stretch_hybrid")) {
command.cmd = "set-stretch";
command.mode = "HYBRID";
} else if(e.target.classList.contains("_ar_stretch_conditional")) {
command.cmd = "set-stretch";
command.mode = "CONDITIONAL";
}
return command;
}
if(e.target.classList.contains("_autoAr")){
if(e.target.classList.contains("_ar_global_options")){
if (e.target.classList.contains("_blacklist")) {
settings.active.arDetect.mode = "blacklist";
} else if (e.target.classList.contains("_whitelist")) {
settings.active.arDetect.mode = "whitelist";
} else {
settings.active.arDetect.mode = "disabled";
}
settings.save();
return;
} else if (e.target.classList.contains("_save_autoAr_timer")) {
var value = parseInt(document.getElementById("_input_autoAr_timer").value.trim());
if(! isNaN(value)){
var timeout = parseInt(value);
settings.active.arDetect.timer_playing = timeout;
settings.save();
}
return;
} else if (e.target.classList.contains("_ar_site_options")) {
var mode;
if(e.target.classList.contains("_disabled")){
mode = "disabled";
} else if(e.target.classList.contains("_enabled")) {
mode = "enabled";
} else {
mode = "default";
}
if(settings.active.sites[site]) {
settings.active.sites[site].arStatus = mode;
} else {
settings.active.sites[site] = {
status: settings.active.extensionMode,
statusEmbedded: settings.active.extensionMode,
arStatus: mode,
type: 'user-defined'
}
}
settings.save();
return;
}
}
if (e.target.classList.contains("_align_ext")) {
if (e.target.classList.contains("_align_ext_left")) {
settings.active.miscFullscreenSettings.videoFloat = 'left';
} else if (e.target.classList.contains("_align_ext_center")) {
settings.active.miscFullscreenSettings.videoFloat = 'center';
} else if (e.target.classList.contains("_align_ext_right")) {
settings.active.miscFullscreenSettings.videoFloat = 'right';
}
settings.save();
return;
}
if (e.target.classList.contains("_align_site")) {
if (!site) {
return;
}
if (e.target.classList.contains("_align_site_left")) {
settings.active.sites[site].videoAlignment = 'left';
} else if (e.target.classList.contains("_align_site_center")) {
settings.active.sites[site].videoAlignment = 'center';
} else if (e.target.classList.contains("_align_site_right")) {
settings.active.sites[site].videoAlignment = 'right';
} else {
// default case — remove this object
delete(settings.active.sites[site].videoAlignment);
}
settings.save();
return;
}
if (e.target.classList.contains("_align")) {
command.cmd = "set-alignment";
if (e.target.classList.contains("_align_video_left")) {
command.mode = 'left';
} else if (e.target.classList.contains("_align_video_center")) {
command.mode = 'center';
} else if (e.target.classList.contains("_align_video_right")) {
command.mode = 'right';
}
return command;
}
//#region zoom buttons
if (e.target.classList.contains("_zoom_show_shortcuts")) {
VideoPanel.misc.zoomShortcuts.classList.remove("hidden");
VideoPanel.buttons.zoom.hideShortcuts.classList.remove("hidden");
VideoPanel.buttons.zoom.showShortcuts.classList.add("hidden");
return;
}
if (e.target.classList.contains("_zoom_hide_shortcuts")) {
VideoPanel.misc.zoomShortcuts.classList.add("hidden");
VideoPanel.buttons.zoom.hideShortcuts.classList.add("hidden");
VideoPanel.buttons.zoom.showShortcuts.classList.remove("hidden");
return;
}
if (e.target.classList.contains("_zoom_reset")) {
zoom_videoScale = scale;
VideoPanel.labels.zoomLevel.textContent = 100;
VideoPanel.inputs.zoomSlider.value = 0; // log₂(1)
command.cmd = 'set-zoom';
command.zoom = 1;
return command;
}
//#endregion
//#region show/hide custom ar
if (e.target.classList.contains("_changeAr_show_customAr")) {
VideoPanel.misc.customArChanger.classList.remove("hidden");
VideoPanel.buttons.changeAr.showCustomAr.classList.add("hidden");
VideoPanel.buttons.changeAr.hideCustomAr.classList.remove("hidden");
return;
}
if (e.target.classList.contains("_changeAr_hide_customAr")) {
VideoPanel.misc.customArChanger.classList.add("hidden");
VideoPanel.buttons.changeAr.showCustomAr.classList.remove("hidden");
VideoPanel.buttons.changeAr.hideCustomAr.classList.add("hidden");
return;
}
//#endregion
}
var command = getcmd(e);
if(command)
port.postMessage(command);
return true;
});
async function sleep(t) {
return new Promise( (resolve,reject) => {
setTimeout(() => resolve(), t);
});
}
async function popup_init() {
// let's init settings and check if they're loaded
await settings.init();
if (Debug.debug) {
console.log("[popup] Are settings loaded?", settings)
}
const customArInputField = document.getElementById("_input_custom_ar");
const autoarFrequencyInputField = document.getElementById("_input_autoAr_timer");
customArInputField.addEventListener("blur", (event) => {
validateCustomAr();
});
customArInputField.addEventListener("mouseleave", (event) => {
validateCustomAr();
});
// autoarFrequencyInputField.addEventListener("blur", (event) => {
// validateAutoArTimeout();
// });
// autoarFrequencyInputField.addEventListener("mouseleave", (event) => {
// validateAutoArTimeout();
// });
hideWarning("script-not-running-warning");
while (true) {
console.log("GETTING SITE")
getSite();
await sleep(5000);
}
}
popup_init();

View File

@ -1,259 +0,0 @@
<html>
<head>
<!-- shit like this really makes you appreciate angular and similar frameworks, doesn't it? -->
<title></title>
<meta charset="utf-8">
<link rel='stylesheet' type='text/css' href='../css/font/overpass.css'>
<link rel='stylesheet' type='text/css' href='../css/font/overpass-mono.css'>
<link rel='stylesheet' type='text/css' href='../css/common.css'>
<link rel='stylesheet' type='text/css' href='./css/popup.css'>
</head>
<body>
<div class="header">
U<span class="smallcaps">ltrawidify</span>: <small>Quick settings</small>
</div>
<!-- TABS/SIDEBAR -->
<div class="left-side">
<div id="_menu_tab_settings_ext" class="menu-item _menu_tab_settings_ext">
Extension settings
</div>
<div id="_menu_tab_settings_site" class="menu-item _menu_tab_settings_site">
Site settings
<span class="menu-item-inline-desc"><br/>Settings for <span id="_menu_tab_settings_site_site_label"></span></span>
</div>
<div id="_menu_tab_settings_video" class="menu-item _menu_tab_settings_video">
Video settings
<span class="menu-item-inline-desc"><br/>Crop & stretch options for current video</span>
</div>
<div id="_menu_tab_about" class="menu-item _menu_tab_about">
About Ultrawidify<span class="menu-item-inline-desc"><br/>See for bug reports</span>
</div>
</div>
<!-- PANELS/CONTENT -->
<div class="right-side">
<div id="script-not-running-warning" class="warning">If you see this line, then your browser didn't start the popup script yet. This is a problem caused by the browser. Please wait a few seconds.</div>
<!-- EXTENSION SETTINGS -->
<div id="_menu_settings_ext" class="suboption hidden">
<p>These settings can be overriden on per-site basis.</p>
<div class="row">
<span class="label">Enable this extension:</span>
<div class="button-row">
<a id="_ext_global_options_blacklist" class="button _ext _ext_global_options _blacklist">Always</a>
<a id="_ext_global_options_whitelist" class="button _ext _ext_global_options _whitelist">On whitelisted sites</a>
<a id="_ext_global_options_disabled" class="button _ext _ext_global_options _disabled" >Never</a>
</div>
<div id="_ext_only_when_ext_enabled">
<div class="row">
<span class="label">Enable autodetection:</span>
<div class="button-row">
<a id="_ar_global_options_blacklist" class="button _autoAr _ar_global_options _blacklist">Always</a>
<a id="_ar_global_options_whitelist" class="button _autoAr _ar_global_options _whitelist">On whitelisted sites</a>
<a id="_ar_global_options_disabled" class="button _autoAr _ar_global_options _disabled" >Never</a>
</div>
</div>
<!-- TODO: default aspect ratio settings -->
<div class="row">
<span class="label experimental">Default stretching mode</span>
<div class="button-row">
<a id="_stretch_global_none" class="button _stretch _ar_stretch_global _none w24">Never<br/><span id="_b_stretch_default_none_key" class="smallcaps small darker"></span></a>
<a id="_stretch_global_basic" class="button _stretch _ar_stretch_global _basic w24">Basic<br/><span id="_b_stretch_default_basic_key" class="smallcaps small darker"></span></a>
<a id="_stretch_global_hybrid" class="button _stretch _ar_stretch_global _hybrid w24">Hybrid<br/><span id="_b_stretch_default_hybrid_key" class="smallcaps small darker"></span></a>
<a id="_stretch_global_conditional" class="button _stretch _ar_stretch_global _conditional w24">Thin borders<br/><span id="_b_stretch_default_conditional_key" class="smallcaps small darker"></span></a>
</div>
</div>
<!-- VIDEO ALIGNMENT -->
<div class="row">
<span class="label">Video alignment:</span>
<div class="button-row">
<a id="_align_ext_left" class="button _align_ext _align_ext_left">Left</a>
<a id="_align_ext_center" class="button _align_ext _align_ext_center">Center</a>
<a id="_align_ext_right" class="button _align_ext _align_ext_right">Right</a>
</div>
</div>
</div>
</div>
</div>
<!-- SITE SETTINGS -->
<div id="_menu_settings_site" class="suboption hidden">
<div class="row">
<span class="label">Options for this site:</span>
<div class="button-row">
<a id="_ext_site_options_whitelist" class="button _ext _ext_site_options _whitelist">Whitelist</a>
<a id="_ext_site_options_default" class="button _ext _ext_site_options _default" >Default</a>
<a id="_ext_site_options_blacklist" class="button _ext _ext_site_options _blacklist">Blacklist</a>
</div>
</div>
<!-- things that are disabled if site is disabled -->
<div id="_site_only_when_site_enabled">
<div class="row">
<span class="label">Automatic detection:</span>
<div class="button-row">
<a id="_ar_site_options_enabled" class="button _autoAr _ar_site_options _enabled" >Whitelist</a>
<a id="_ar_site_options_default" class="button _autoAr _ar_site_options _default" >Default</a>
<a id="_ar_site_options_disabled" class="button _autoAr _ar_site_options _disabled">Blacklist</a>
</div>
</div>
<!-- TODO: default aspect ratio, default settings -->
<!-- STRETCH MODE -->
<div class="row">
<span class="label experimental">Default stretching mode</span>
<div class="button-row">
<a id="_stretch_site_none" class="button _stretch _ar_stretch_site _none w24">Never<br/><span id="_b_stretch_default_none_key" class="smallcaps small darker"></span></a>
<a id="_stretch_site_basic" class="button _stretch _ar_stretch_site _basic w24">Basic<br/><span id="_b_stretch_default_basic_key" class="smallcaps small darker"></span></a>
<a id="_stretch_site_hybrid" class="button _stretch _ar_stretch_site _hybrid w24">Hybrid<br/><span id="_b_stretch_default_hybrid_key" class="smallcaps small darker"></span></a>
<a id="_stretch_site_conditional" class="button _stretch _ar_stretch_site _conditional w24">Thin borders<br/><span id="_b_stretch_default_conditional_key" class="smallcaps small darker"></span></a>
<a id="_stretch_site_default" class="button _stretch _ar_stretch_site _default w24">Default<br/><span id="_b_stretch_default_conditional_key" class="smallcaps small darker"></span></a>
</div>
</div>
<!-- VIDEO ALIGNMENT -->
<div class="row">
<span class="label">Video alignment:</span>
<div class="button-row">
<a id="_align_site_left" class="button _align_site _align_site_left">Left</a>
<a id="_align_site_center" class="button _align_site _align_site_center">Center</a>
<a id="_align_site_right" class="button _align_site _align_site_right">Right</a>
<a id="_align_site_default"class="button _align_site _align_site_default">Default</a>
</div>
</div>
</div>
</div>
<!-- VIDEO SETTINGS -->
<div id="_menu_settings_video" class="suboption">
<!-- CROP STUFF -->
<div class="row">
<span class="label">Cropping mode</span>
<div class="button-row">
<a class="button _changeAr _ar_auto w24">Auto-detect<br/><span id="_b_changeAr_auto-ar_key" class="smallcaps small darker"></span>&nbsp;</a>
<a class="button _changeAr _ar_reset w24">Reset<br/><span id="_b_changeAr_reset_key" class="smallcaps small darker">&nbsp;</span></a>
<a class="button _changeAr _ar_219 w24">21:9<br/><span id="_b_changeAr_219_key" class="smallcaps small darker">&nbsp;</span></a>
<a class="button _changeAr _ar_189 w24">2:1 (18:9)<br/><span id="_b_changeAr_189_key" class="smallcaps small darker">&nbsp;</span></a>
<a class="button _changeAr _ar_169 w24">16:9<br/><span id="_b_changeAr_169_key" class="smallcaps small darker">&nbsp;</span></a>
<a class="button _changeAr _ar_custom w24">Custom<br/><span id="_b_changeAr_custom_key" class="smallcaps small darker">&nbsp;</span></a>
</div>
<div style="overflow: auto">
<div class="float-right medium-small">
<a id="_changeAr_b_show_customAr" class="_changeAr_show_customAr x-pad-1em">Set custom aspect ratio</a>
<a id="_changeAr_b_hide_customAr" class="_changeAr_hide_customAr hidden x-pad-1em">Hide custom aspect ratio settings</a>
</div>
</div>
</div>
<div id="_changeAr_customAr" class="row hidden">
<span class="label">Custom aspect ratio</span>
<div>
<input id="_input_custom_ar" class="_changeAr _custom" onblur="validateCustomAr()"> &nbsp; &nbsp; <span id="_b_changeAr_save_custom_ar" class="button _ar_save_custom_ar _changeAr">Save</span>
<br/><small>Aspect ratio can be entered in any of these forms (without quotes):
<ul style="display: block; padding-top: 0px; margin-top: 0px">
<li>fraction (<i>width/height</i>, e.g. <i>16/10</i>, <i>2560/1080</i>, etc.)</li>
<li>ratio (e.g. <i>1:2.35</i> or simply <i>2.35</i><i>1:</i> bit can be left out)</li>
</ul></small>
</div>
</div>
<!-- ZOOM STUFF -->
<span class="label experimental">Manual zooming and panning</span>
<div class="row">
<!--
min, max and value need to be implemented in js as this slider
should use logarithmic scale
-->
<input id="_input_zoom_slider" class="w100"
type="range"
step="any"
/>
<div style="overflow: auto">
<div class="inline-block float-left medium-small x-pad-1em">
Zoom: <span id="_label_zoom_level">100</span>%
</div>
<div class="inline-block float-right medium-small">
<a id="_zoom_b_show_shortcuts" class="_zoom_show_shortcuts x-pad-1em">show shortcuts</a>
<a id="_zoom_b_hide_shortcuts" class="_zoom_hide_shortcuts hidden x-pad-1em">hide shortcuts</a>
<a class="_zoom_reset x-pad-1em">reset</a>
</div>
</div>
<!-- <div class="m-t-0-33em w100 display-block">
<input id="_input_zoom_site_allow_pan"
type="checkbox"
/>
Pan with mouse
</div> -->
<div id="_zoom_shortcuts" class="row hidden">
<small>Keyboard shortcuts:<ul>
<li>Z: zoom</li>
<li>U: unzoom</li>
<li>P: toggle pan</li>
<li>shift: hold to enable/prevent pan</li>
</ul></small>
</div>
</div>
<!-- STRETCH STUFF -->
<span class="label experimental">Stretching mode</span>
<div class="button-row">
<a class="button _stretch _ar_stretch_none w24">Normal<br/><span id="_b_stretch_none_key" class="smallcaps small darker"></span></a>
<a class="button _stretch _ar_stretch_basic w24">Basic<br/><span id="_b_stretch_basic_key" class="smallcaps small darker"></span></a>
<a class="button _stretch _ar_stretch_hybrid w24">Hybrid<br/><span id="_b_stretch_hybrid_key" class="smallcaps small darker"></span></a>
<a class="button _stretch _ar_stretch_conditional w24">Thin borders<br/><span id="_b_stretch_conditional_key" class="smallcaps small darker"></span></a>
</div>
<!-- VIDEO ALIGNMENT -->
<div class="row">
<span class="label">Video alignment:</span>
<div class="button-row">
<a id="_align_video_left" class="button _align _align_video_left">Left</a>
<a id="_align_video_center" class="button _align _align_video_center">Center</a>
<a id="_align_video_right" class="button _align _align_video_right">Right</a>
</div>
</div>
</div>
<!-- ABOUTR -->
<div id="_menu_about" class="suboption hidden">
<div class="row">
<span class="label">Ultrawidify version:</span><br/> <span id="uw-version"></span>
</div>
<div class="row">
<span class="label">Having an issue?</span><br/> Report <strike>undocumented features</strike> bugs using one of the following options:<ul><li> <a target="_blank" href="https://github.com/xternal7/ultrawidify/issues"><b>Github (strongly preferred)</b></a><br/></li>
<li>PM me on <a target="_blank" href="https://www.reddit.com/message/compose?to=xternal7&subject=[Ultrawidify]%20ENTER%20SUMMARY%20OF%20YOUR%20PROBLEM%20HERE&message=Describe+your+issue+in+more+detail.+Don%27t+forget+to+include%3A%0D%0A%2A+Extension+version%0D%0A%2A+Browser%0D%0A%2A+Operating+system%0D%0A%2A+Other+extensions+that+could+possibly+interfere%0D%0A%0D%0AIf+you%27re+reporting+an+issue+with+automatic+aspect+ratio+detection%2C+please+also+include+the+following+%28if+possible%29%3A%0D%0A%2A+model+and+make+of+your+CPU%0D%0A%2A+amount+of+RAM">reddit</a><br/></li>
<li>Email: <a target="_blank" href="mailto:tamius.han@gmail.com?subject=%5BUltrawidify%5D+ENTER+SUMMARY+OF+YOUR+ISSUE+HERE&body=Describe+your+issue+in+more+detail.+Don%27t+forget+to+include%3A%0D%0A%2A+Extension+version%0D%0A%2A+Browser%0D%0A%2A+Operating+system%0D%0A%2A+Other+extensions+that+could+possibly+interfere%0D%0A%0D%0AIf+you%27re+reporting+an+issue+with+automatic+aspect+ratio+detection%2C+please+also+include+the+following+%28if+possible%29%3A%0D%0A%2A+model+and+make+of+your+CPU%0D%0A%2A+amount+of+RAM">tamius.han@gmail.com</a></li>
</ul>
</div>
</div>
</div>
<!-- load all scripts. ordering is important! -->
<script src="../../js/conf/Debug.js"></script>
<script src="../../js/lib/BrowserDetect.js"></script>
<script src="../../js/lib/Comms.js"></script>
<script src="../../js/conf/ExtensionConf.js"></script>
<script src="../../js/lib/Settings.js"></script>
<script src="./js/popup.js"></script>
</body>
</html>

View File

@ -1,202 +0,0 @@
@import url("../css/font/overpass.css");
@import url("../css/font/overpass-mono.css");
body {
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAMAAAAp4XiDAAAAUVBMVEWFhYWDg4N3d3dtbW17e3t1dXWBgYGHh4d5eXlzc3OLi4ubm5uVlZWPj4+NjY19fX2JiYl/f39ra2uRkZGZmZlpaWmXl5dvb29xcXGTk5NnZ2c8TV1mAAAAG3RSTlNAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAvEOwtAAAFVklEQVR4XpWWB67c2BUFb3g557T/hRo9/WUMZHlgr4Bg8Z4qQgQJlHI4A8SzFVrapvmTF9O7dmYRFZ60YiBhJRCgh1FYhiLAmdvX0CzTOpNE77ME0Zty/nWWzchDtiqrmQDeuv3powQ5ta2eN0FY0InkqDD73lT9c9lEzwUNqgFHs9VQce3TVClFCQrSTfOiYkVJQBmpbq2L6iZavPnAPcoU0dSw0SUTqz/GtrGuXfbyyBniKykOWQWGqwwMA7QiYAxi+IlPdqo+hYHnUt5ZPfnsHJyNiDtnpJyayNBkF6cWoYGAMY92U2hXHF/C1M8uP/ZtYdiuj26UdAdQQSXQErwSOMzt/XWRWAz5GuSBIkwG1H3FabJ2OsUOUhGC6tK4EMtJO0ttC6IBD3kM0ve0tJwMdSfjZo+EEISaeTr9P3wYrGjXqyC1krcKdhMpxEnt5JetoulscpyzhXN5FRpuPHvbeQaKxFAEB6EN+cYN6xD7RYGpXpNndMmZgM5Dcs3YSNFDHUo2LGfZuukSWyUYirJAdYbF3MfqEKmjM+I2EfhA94iG3L7uKrR+GdWD73ydlIB+6hgref1QTlmgmbM3/LeX5GI1Ux1RWpgxpLuZ2+I+IjzZ8wqE4nilvQdkUdfhzI5QDWy+kw5Wgg2pGpeEVeCCA7b85BO3F9DzxB3cdqvBzWcmzbyMiqhzuYqtHRVG2y4x+KOlnyqla8AoWWpuBoYRxzXrfKuILl6SfiWCbjxoZJUaCBj1CjH7GIaDbc9kqBY3W/Rgjda1iqQcOJu2WW+76pZC9QG7M00dffe9hNnseupFL53r8F7YHSwJWUKP2q+k7RdsxyOB11n0xtOvnW4irMMFNV4H0uqwS5ExsmP9AxbDTc9JwgneAT5vTiUSm1E7BSflSt3bfa1tv8Di3R8n3Af7MNWzs49hmauE2wP+ttrq+AsWpFG2awvsuOqbipWHgtuvuaAE+A1Z/7gC9hesnr+7wqCwG8c5yAg3AL1fm8T9AZtp/bbJGwl1pNrE7RuOX7PeMRUERVaPpEs+yqeoSmuOlokqw49pgomjLeh7icHNlG19yjs6XXOMedYm5xH2YxpV2tc0Ro2jJfxC50ApuxGob7lMsxfTbeUv07TyYxpeLucEH1gNd4IKH2LAg5TdVhlCafZvpskfncCfx8pOhJzd76bJWeYFnFciwcYfubRc12Ip/ppIhA1/mSZ/RxjFDrJC5xifFjJpY2Xl5zXdguFqYyTR1zSp1Y9p+tktDYYSNflcxI0iyO4TPBdlRcpeqjK/piF5bklq77VSEaA+z8qmJTFzIWiitbnzR794USKBUaT0NTEsVjZqLaFVqJoPN9ODG70IPbfBHKK+/q/AWR0tJzYHRULOa4MP+W/HfGadZUbfw177G7j/OGbIs8TahLyynl4X4RinF793Oz+BU0saXtUHrVBFT/DnA3ctNPoGbs4hRIjTok8i+algT1lTHi4SxFvONKNrgQFAq2/gFnWMXgwffgYMJpiKYkmW3tTg3ZQ9Jq+f8XN+A5eeUKHWvJWJ2sgJ1Sop+wwhqFVijqWaJhwtD8MNlSBeWNNWTa5Z5kPZw5+LbVT99wqTdx29lMUH4OIG/D86ruKEauBjvH5xy6um/Sfj7ei6UUVk4AIl3MyD4MSSTOFgSwsH/QJWaQ5as7ZcmgBZkzjjU1UrQ74ci1gWBCSGHtuV1H2mhSnO3Wp/3fEV5a+4wz//6qy8JxjZsmxxy5+4w9CDNJY09T072iKG0EnOS0arEYgXqYnXcYHwjTtUNAcMelOd4xpkoqiTYICWFq0JSiPfPDQdnt+4/wuqcXY47QILbgAAAABJRU5ErkJggg==);
background-size: 75px;
background-color: #000000;
color: #ddd;
font-family: 'overpass', serif;
font-size: 1.2em;
width: 100%;
border: 0px;
margin: 0px;
padding: 0px;
text-align: center;
z-index: -1000;
}
a, a:visited{
color: #fa6607;
text-decoration: none;
}
a:hover{
color: #fa6;
}
.show{
display: block !important;
}
.hide{
display: none !important;
}
head{
width: 100%;
border: 0px;
margin: 0px;
padding: 0px;
}
h1,h2{
color: #ff9;
font-weight: 300;
}
h1{
font-size: 3.3em;
}
h2{
font-size: 2.2em;
}
.sites_header{
font-size: 1.6em;
color: #ff9;
}
.content{
display: inline-block;
width: 52em;
}
.center{
margin: 0 auto;
}
.basecolor {
color: #ddd !important;
}
.red{
color: #ff3a00;
}
.disabled {
opacity: 0.69;
}
.left {
text-align: left;
}
.block {
display: inline-block;
margin-left: 1em;
margin-right: 1em;
}
.tabline {
background-color: #000;
width: 100%;
margin-bottom: 1.5em;
padding-top: 0.3em;
padding-bottom: 0.4em;
z-index: -200;
}
.tab {
color: #fa6607;
}
.tab:hover{
text-shadow: #aa5 0px 0px 0.02em,#aa5 0px 0px 0.02em;
}
.tab-selected {
color: #ff9 !important;
}
#all{
min-width: 100%;
min-height: 100vh;
background-image: url('img/settings/about-bg.png');
background-position: calc(50vw + 20em) 10vh;
background-attachment: fixed;
background-size: auto 100%;
background-repeat: no-repeat;
}
.dup_keybinds{
background-color: #720 !important;
}
input[type=text]{
font-size: 1em;
padding-left: 0.6em;
margin-left: 1em;
width: 2em;
background-color: #000;
border: 1px #442 solid;
}
.uw_shortcuts_line{
padding-top: 0.25em;
padding-left: 5em;
}
.uw_shortcuts_label{
display: inline-block;
color: #fff;
width: 17.5em;
}
.uw_options_line{
margin-top: 0.75em;
font-size: 1.1em;
width: 80%;
margin-left: 5%;
}
.uw_options_option{
margin-left: 5%;
}
.uw_suboption{
margin-left: 5em;
margin-top: 0.75em;
font-size: 0.85em;
}
.uw_options_desc, .uw_suboption_desc{
margin-top: 0.33em;
font-size: 0.69em;
color: #aaa;
}
.uw_suboption_desc{
margin-left: 5em;
}
.buttonbar{
/* width: 100%; */
padding-left: 20em;
margin-bottom: 2em;
}
.button{
display: inline-block;
margin-left: 1em;
margin-right: 1em;
padding-left: 1em;
padding-right: 1em;
padding-top: 0.4em;
width: 4em;
text-align: center;
background-color: rgba(0,0,0,0.66);
color: #ffc;
height: 1.7em;
}
/** site options css **/
.site_name {
padding-left: 1em;
padding-bottom: 0.3em;
color: #fff;
font-size: 1.1em;
height: 13em !important;
}
.site_details {
font-size: 0.8em;
}
.site_title_ebox {
width: 10em !important;
font-size: 1.5em !important;
background-color: rgba(0,0,0,0) !important;
margin-left: 0px !important;
border: 0px !important;
color: #ffc !important;
}
.details_ebox {
width: 12em !important;
background-color: rgba(0,0,0,0) !important;
border: 0px !important;
color: #ffc !important;
margin-left: 0em !important;
}
.details_ebox:disabled {
color: #aaa !important;
}
.inline_button {
display: inline-block;
margin-top: -1.42em;
}
.inline_button:hover {
color: #fff;
}

View File

@ -1,292 +0,0 @@
<html>
<head>
<title>Ultrawidify :: settings</title>
<meta charset="UTF-8">
<base target="_blank">
<style>
</style>
</head>
<body>
<div id="all">
<header>
<div class="content center">
<div class="left">
<h1>Ultrawidify :: settings</h1>
</div>
</div>
<div class="tabline center">
<div class="content left">
<div id="tab_general_settings" class="block tab tab-selected">General settings</div>
<div id="tab_sites" class="block tab">Site options</div>
<div id="tab_shortcuts" class="block tab">Shortcuts</div>
<div id="tab_about" class="block tab">About</div>
</div>
</div>
</header>
<!-- GENERAL SETTINGS -->
<div id="general_settings">
<div class="content left">
<div class="row">
<span class="label">Enable this extension:</span>
<div class="button-row">
<a id="_ext_global_options_blacklist" class="button _ext _ext_global_options _blacklist">Always</a>
<a id="_ext_global_options_whitelist" class="button _ext _ext_global_options _whitelist">On whitelisted sites</a>
<a id="_ext_global_options_disabled" class="button _ext _ext_global_options _disabled" >Never</a>
</div>
<span class="description">
<b>Always</b> enables this extension on every site you visit that you didn't blacklist.
<b>On whitelisted sites</b> enables this extension only on sites you explicitly whitelisted.
<b>Never</b> disables extension on all sites, even on those you whitelisted.
</span>
</div>
<div class="row">
<span class="label">Enable autodetection:</span>
<div class="button-row">
<a id="_general_ar_global_options_blacklist" class="button _autoAr _ar_global_options _blacklist">Always</a>
<a id="_general_ar_global_options_whitelist" class="button _autoAr _ar_global_options _whitelist">On whitelisted sites</a>
<a id="_general_ar_global_options_disabled" class="button _autoAr _ar_global_options _disabled" >Never</a>
</div>
<span class="description">
<b>Always</b> enables autodetection on every site this extension is enabled for, unless blacklisted.
<b>On whitelisted sites</b> enables autodetection only for sites that you explicitly enabled.
<b>Never</b> disables autodetection on all sites, even on those you whitelisted.
</span>
</div>
<div class="row">
<span class="label">In-player UI</span>
<div class="button-row">
<a id="_ar_global_options_blacklist" class="button _ui _ui_global_options _blacklist">Always/a>
<a id="_ar_global_options_whitelist" class="button _ui _ui_global_options _whitelist">On whitelisted sites</a>
<a id="_ar_global_options_disabled" class="button _ui _ui_global_options _disabled" >Never</a>
</div>
</div>
<div class="row">
<span class="label">Video alignment:</span>
<div class="button-row">
<a id="_align_left" class="button _align _align_left">left</a>
<a id="_align_center" class="button _align _align_center">center</a>
<a id="_align_right" class="button _align _align_right">right</a>
</div>
</div>
<div class="row">
<div class="button-row">
<a id="_stretch_global_none" class="button _stretch _ar_stretch_global _none">Never<br/><span id="_b_stretch_default_none_key" class="smallcaps small darker"></span></a>
<a id="_stretch_global_basic" class="button _stretch _ar_stretch_global _basic">Basic<br/><span id="_b_stretch_default_basic_key" class="smallcaps small darker"></span></a>
<a id="_stretch_global_hybrid" class="button _stretch _ar_stretch_global _hybrid">Hybrid<br/><span id="_b_stretch_default_hybrid_key" class="smallcaps small darker"></span></a>
<a id="_stretch_global_conditional" class="button _stretch _ar_stretch_global _conditional">Thin borders<br/><span id="_b_stretch_default_conditional_key" class="smallcaps small darker"></span></a>
</div>
</div>
</div>
</div>
</div>
<!-- AUTOMATIC DETECTION SETTINGS -->
<div id="autoar_settings">
<div class="row">
<span class="label">Enable autodetection:</span>
<div class="button-row">
<a id="_ar_global_options_blacklist" class="button _autoAr _ar_global_options _blacklist">Always</a>
<a id="_ar_global_options_whitelist" class="button _autoAr _ar_global_options _whitelist">On whitelisted sites</a>
<a id="_ar_global_options_disabled" class="button _autoAr _ar_global_options _disabled" >Never</a>
</div>
<div class="description">
<b>Always</b> enables autodetection on every site this extension is enabled for, unless blacklisted.
<b>On whitelisted sites</b> enables autodetection only for sites that you explicitly enabled.
<b>Never</b> disables autodetection on all sites, even on those you whitelisted.
</div>
</div>
<div class="row">
<span class="label">Checking for aspect ratio changes</span>
<div>
When video is playing: <input id="_input_autoAr_timer" class="_autoAr _autoAr_timer" type="number" min="5" max="10000"> ms
<div>
<div>
When video is paused: <input id="_input_autoAr_timer_pause" class="_autoAr _autoAr_timer_pause" type="number" min="5" max="10000"> ms
<div>
<div>
On error: <input id="_input_autoAr_timer_error" class="_autoAr _autoAr_timer_error" type="number" min="5" max="10000"> ms
<div>
<div class="description">
All values are in milliseconds.
The values determine how often the autodetection script will scan video for changes in aspect ratio under different circumstances.
Using shorter intervals can cause lag and increases resource usage.
Using longer intervals increases delay between aspect ratio changing and said change being detected.
</div>
</div>
<div id="_autoar_fallback_mode_top" class="row">
<label>Enable fallback mode</label>
todo: insert checkbox
<div class="description">
<p>In order for autodetection to work, the extension grabs a frame of the video you're watching and takes a look at it.
Some websites use DRM, which prevents you from using the HTML5-blessed way of getting a frame from a video.
Some browsers offer some features that can be used to circumvent DRM protection.
With this option enabled, the extension will use said features to basically do that.
</p>
<p><b>Technical details: tl;dr — only works in Firefox and maybe its forks.</b><br/>
Fallback mode depends on two things:
<ol>
<li>Attempting to grab a video frame from DRM protected video stream must return an error (Chrome returns black image)</li>
<li>Supports <a href="https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/drawWindow" target="_blank">CanvasRenderingContext2D.drawWindow()</a> method, which pretty much only Firefox does</li>
</ol>
</p>
<p><b>But I thought it's <a href="http://www.dmlp.org/legal-guide/circumventing-copyright-controls" target="_blank">illegal</a> to circumvent DRM?</b><br/>
Courts <a href="https://arstechnica.com/information-technology/2010/07/court-breaking-drm-for-a-fair-use-is-legal/" target="_blank">disagree</a> (at least as far as our use-case is concerned).
At this point I'd also throw in some choice words about DRM and everyone using it, but AMO gives me "you're breaking terms of conduct" warnings over use of the word for thing you flush down the toilet, prepended by a word for male cow*, in the code comments.<small><br/>*'cow' is term for the female</small>
</p>
</div>
</div>
</div>
<!-- INTERFACE AND SHORTCUTS -->
<div id="interface_shortcut_settings">
</div>
<!-- SITE SETTINGS -->
<div id="uw_sites" class="hide">
<h1>PLEASE NOTE: THIS IS COMPLETELY NON-FUNCTIONAL ATM. YOU CANT SET SITE-SPECIFIC OPTIONS ATM</h1>
<div id="uw_sites_body" class="content left">
</div>
</div>
<!-- REMOVE PLOX -->
<div id="uw_shortcuts" class="hide">
<div class="content left">
<h2>Keyboard shortcuts</h2>
<form>
<!-- BEGIN FORM -->
<!-- try autoar -->
<div class="uw_shortcuts_line">
<div class="uw_shortcuts_label">Autodetect aspect ratio</div>
<input type='checkbox' id="_kbd_autoar_ctrlKey" > Ctrl +
<input type='checkbox' id="_kbd_autoar_shiftKey" > Shift +
<input type='checkbox' id="_kbd_autoar_altKey" > Alt +
<input type='text' id="_kbd_autoar_lettr" maxlength='1' class='shortcut_input'>
</div>
<!-- fit width -->
<div class="uw_shortcuts_line">
<div class="uw_shortcuts_label">Fit to width</div>
<input type='checkbox' id="_kbd_fitw_ctrlKey" > Ctrl +
<input type='checkbox' id="_kbd_fitw_shiftKey" > Shift +
<input type='checkbox' id="_kbd_fitw_altKey" > Alt +
<input type='text' id="_kbd_fitw_lettr" maxlength='1' class='shortcut_input'>
</div>
<!-- fit height -->
<div class="uw_shortcuts_line">
<div class="uw_shortcuts_label">Fit to height</div>
<input type='checkbox' id="_kbd_fith_ctrlKey" > Ctrl +
<input type='checkbox' id="_kbd_fith_shiftKey" > Shift +
<input type='checkbox' id="_kbd_fith_altKey" > Alt +
<input type='text' id="_kbd_fith_lettr" maxlength='1' class='shortcut_input'>
</div>
<!-- reset -->
<div class="uw_shortcuts_line">
<div class="uw_shortcuts_label">Reset</div>
<input type='checkbox' id="_kbd_reset_ctrlKey" > Ctrl +
<input type='checkbox' id="_kbd_reset_shiftKey" > Shift +
<input type='checkbox' id="_kbd_reset_altKey" > Alt +
<input type='text' id="_kbd_reset_lettr" maxlength='1' class='shortcut_input'>
</div>
<!-- force 21:9 -->
<div class="uw_shortcuts_line">
<div class="uw_shortcuts_label">Force 21:9</div>
<input type='checkbox' id="_kbd_char_219_ctrlKey" > Ctrl +
<input type='checkbox' id="_kbd_char_219_shiftKey" > Shift +
<input type='checkbox' id="_kbd_char_219_altKey" > Alt +
<input type='text' id="_kbd_char_219_lettr" maxlength='1' class='shortcut_input'>
</div>
<!-- force 2:1 -->
<div class="uw_shortcuts_line">
<div class="uw_shortcuts_label">Force 2:1 (18:9)</div>
<input type='checkbox' id="_kbd_char_189_ctrlKey" > Ctrl +
<input type='checkbox' id="_kbd_char_189_shiftKey" > Shift +
<input type='checkbox' id="_kbd_char_189_altKey" > Alt +
<input type='text' id="_kbd_char_189_lettr" maxlength='1' class='shortcut_input'>
</div>
<!-- force 16:9 -->
<div class="uw_shortcuts_line">
<div class="uw_shortcuts_label">Force 16:9</div>
<input type='checkbox' id="_kbd_char_169_ctrlKey" > Ctrl +
<input type='checkbox' id="_kbd_char_169_shiftKey" > Shift +
<input type='checkbox' id="_kbd_char_169_altKey" > Alt +
<input type='text' id="_kbd_char_169_lettr" maxlength='1' class='shortcut_input'>
</div>
<!-- END FORM -->
</form>
<div class="buttonbar"><div class="button" id="kb_cancel">Cancel</div><div class="button" id="kb_save">Save</div></div>
<div id="errbox"></div>
<div><small><b>Note:</b> keyboard shortcut conflicts aren't handled by this extension. You must know your shortcuts.<br/><b>Pro tip:</b> If you don't want to have a keybind for a shortcut, just uncheck all boxes for the action and remove the letter</small></div>
</div>
</div>
<!-- ABOUT -->
<div id="about" class="hide">
<div class="content left">
<h2>Ultrawidify - an aspect ratio fixer for youtube <small>(and netflix)</small></h2>
<p>Created by Tamius Han (me). If something is broken, you can shout at me on <a href="https://github.com/xternal7/ultrawidify">github</a> (shout nicely, though. Open an issue or bug report or something).
If you're curious to see the source code, <a href="https://github.com/xternal7/ultrawidify">github's here</a>.
If you're looking at this page because you're bored and want to be bored some more, <a href="http://tamius.net">my website's here</a>.</p>
<h2>So you want to help?</h2>
<p>Alan pls add quick description.</p>
<h2>Plans for the future</h2>
<h2>Acknowledgements</h2>
<p>This extension uses font <a href="http://overpassfont.org/">Overpass</a>.</p>
<!-- <p>Special thanks to CD Project Red (The Witcher 2), Cyanide Studios (Styx, Of Orcs and Men), and Valve (CS:GO), which made it possible for me to untrigger myself after seeing so many improperly encoded videos.</p> -->
<!-- <small><p>More or less.</p> -->
<!-- <p>GW2 is also roughly how the avatar was obtained. Noone tell Anet, though.</p></small> -->
<!-- <p>Special one-finger salute to all incompetent people who don't know how to properly encode videos and upload them to youtube (to word it most nicely).</p> -->
<p>Special thanks to me for making this extension. You're welcome.</p>
</div>
</div>
</div>
<script src="../../js/conf/Debug.js"></script>
<script src="../../js/lib/BrowserDetect.js"></script>
<script src="../../js/lib/StorageManager.js"></script>
<script src="../../js/conf/Keybinds.js"></script>
<script src="../../js/conf/Settings.js"></script>
<script src="./settings.js"></script>
</body>
</html>

View File

@ -1,134 +0,0 @@
function showAbout(){
clearPage();
document.getElementById("about").classList.remove("hide");
document.getElementById("tab_about").classList.add("tab-selected");
}
function showShortcuts(){
clearPage();
document.getElementById("uw_shortcuts").classList.remove("hide");
document.getElementById("tab_shortcuts").classList.add("tab-selected");
}
function showGeneralSettings(){
clearPage();
document.getElementById("general_settings").classList.remove("hide");
document.getElementById("tab_general_settings").classList.add("tab-selected");
}
function showSites(){
clearPage();
document.getElementById("uw_sites").classList.remove("hide");
document.getElementById("tab_sites").classList.add("tab-selected");
}
function clearPage(){
// Hide you sections
document.getElementById("uw_shortcuts").classList.add("hide");
document.getElementById("about").classList.add("hide");
document.getElementById("general_settings").classList.add("hide");
document.getElementById("uw_sites").classList.add("hide");
// Hide you tabs
document.getElementById("tab_shortcuts").classList.remove("tab-selected");
document.getElementById("tab_about").classList.remove("tab-selected");
document.getElementById("tab_general_settings").classList.remove("tab-selected");
document.getElementById("tab_sites").classList.remove("tab-selected");
}
async function loadKeybinds(){
// load showShortcuts
var keybinds = await Keybinds.fetch();
for(var key in keybinds){
if(Debug.debug)
console.log("[settings.js::loadKeybinds] we're looking at this key:", key, "it splits like this:", key.toLowerCase().split("_"));
var keypressArr = key.split("_");
var opts = keybinds[key];
var query = "_kbd_" + opts.action + "_";
if(opts.action == "char"){
if(opts.targetAr == 2.39)
query += ("219_");
else if(opts.targetAr == 2.0)
query += ("189_");
else if(opts.targetAr == 1.78)
query += ("169_");
}
var q2;
for(var modKey of Keybinds.mods){
q2 = "#" + query + modKey;
document.querySelector(q2).checked = keypressArr.indexOf(modKey) != -1;
}
q2 = "#" + query + "lettr";
document.querySelector(q2).value = keypressArr[keypressArr.length - 1].toLowerCase();
}
}
async function saveKeybinds(){
var actions = [ "autoar", "fitw", "fith", "reset", "char_219", "char_189", "char_169" ];
var savedShortcuts = {};
for(var action of actions){
var queryBase = "#_kbd_" + action;
var letter = document.querySelector(queryBase + "_lettr").value.trim();
if(letter === "" || letter === undefined)
continue; // we don't make a shortcut for this action
var shortcutKeypress = "";
for(mod of Keybinds.mods)
if(document.querySelector(queryBase + "_" + mod).checked)
shortcutKeypress += (mod + "_");
shortcutKeypress += letter;
savedShortcuts[shortcutKeypress] = {};
if(action.startsWith("char_")){
savedShortcuts[shortcutKeypress].action = "char";
if(action == "char_219")
savedShortcuts[shortcutKeypress].targetAr = 2.39;
else if(action == "char_189")
savedShortcuts[shortcutKeypress].targetAr = 2.0;
else if(action == "char_169")
savedShortcuts[shortcutKeypress].targetAr = 1.78;
}
else{
savedShortcuts[shortcutKeypress].action = action;
}
}
// out with the old
await StorageManager.delopt("keybinds");
//in with the new
StorageManager.setopt({"keybinds":savedShortcuts});
}
// page init
// document.addEventListener("DOMContentLoaded", loadopts);
document.querySelector("#tab_shortcuts").addEventListener("click", showShortcuts);
document.querySelector("#tab_about").addEventListener("click", showAbout);
// document.querySelector("#tab_general_settings").addEventListener("click",showGeneralSettings);
// document.querySelector("#tab_sites").addEventListener("click", showSites);
document.querySelector("#kb_save").addEventListener("click", saveKeybinds);
document.querySelector("#kb_cancel").addEventListener("click", loadKeybinds);
// document.querySelector("#enable_autoar").addEventListener("click",saveAutoar);
// document.querySelector("#enable_ui").addEventListener("click", saveUI);
// document.querySelector("#enable_ui_compact").addEventListener("click", saveUI);
loadKeybinds();

53
scripts/build-zip.js Normal file
View File

@ -0,0 +1,53 @@
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const archiver = require('archiver');
const DEST_DIR = path.join(__dirname, '../dist');
const DEST_ZIP_DIR = path.join(__dirname, '../dist-zip');
const extractExtensionData = () => {
const extPackageJson = require('../package.json');
return {
name: extPackageJson.name,
version: extPackageJson.version
}
};
const makeDestZipDirIfNotExists = () => {
if(!fs.existsSync(DEST_ZIP_DIR)) {
fs.mkdirSync(DEST_ZIP_DIR);
}
}
const buildZip = (src, dist, zipFilename) => {
console.info(`Building ${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)
.on('error', err => reject(err))
.pipe(stream);
stream.on('close', () => resolve());
archive.finalize();
});
};
const main = () => {
const {name, version} = extractExtensionData();
const zipFilename = `${name}-v${version}.zip`;
makeDestZipDirIfNotExists();
buildZip(DEST_DIR, DEST_ZIP_DIR, zipFilename)
.then(() => console.info('OK'))
.catch(console.err);
};
main();

55
scripts/remove-evals.js Normal file
View File

@ -0,0 +1,55 @@
#!/usr/bin/env node
const path = require('path');
const fs = require('fs');
const BUNDLE_DIR = path.join(__dirname, '../dist');
const bundles = [
'background.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;
const evalRegexForDevelopment = /;\\r\\n\\r\\n\/\/ This works in non-strict mode(?:.){1,304}/g;
const removeEvals = (file) => {
console.info(`Removing eval() from ${file}`);
return new Promise((resolve, reject) => {
fs.readFile(file, 'utf8', (err, data) => {
if(err) {
reject(err);
return;
}
const regex = process.env.NODE_ENV === 'production' ? evalRegexForProduction : evalRegexForDevelopment;
if(!regex.test(data)) {
reject(`No CSP specific code found in ${file}.`);
return;
}
data = data.replace(regex, '=window;');
fs.writeFile(file, data, (err) => {
if(err) {
reject(err);
return;
}
resolve();
});
});
});
};
const main = () => {
bundles.forEach(bundle => {
removeEvals(path.join(BUNDLE_DIR, bundle))
.then(() => console.info(`Bundle ${bundle}: OK`))
.catch(console.error);
});
};
main();

View File

@ -0,0 +1,224 @@
<template>
<div class="action flex flex-column">
<div class="flex flex-row action-name-cmd-container">
<div class="">
</div>
<div class="flex action-name">
<span class="icon red" @click="removeAction()">🗙</span> &nbsp; &nbsp;
<span class="icon" @click="editAction()">🖉</span> &nbsp; &nbsp;
{{action.name}}
</div>
</div>
<div class="flex flex-row command-details">
<div class="flex flex-column cmd-container">
<div class="flex bold">
Command:
</div>
<div class="flex cmdlist">
{{parseCommand(action.cmd)}}
</div>
</div>
<!-- SCOPES -->
<div class="flex flex-column scopes-container">
<div class="flex bold">Popup scopes:</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 KeyboardShortcutParser from '../js/KeyboardShortcutParser';
export default {
data () {
return {
Stretch: Stretch
}
},
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';
.action {
cursor: pointer;
}
.action .command-details {
height: 0px;
max-height: 0px;
transition: max-height 0.5s ease;
overflow: hidden;
transition: height 0.5s ease;
}
.action:hover .command-details {
height: auto;
max-height: 200px;
transition: max-height 0.5s ease;
}
.action-name-cmd-container {
padding: 1rem;
}
.action-name {
font-size: 1.5rem;
font-weight: 300;
color: $text-normal;
}
.action-name:hover {
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;
}
</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>

View File

@ -0,0 +1,30 @@
<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 scoped>
.center-text {
text-align: center;
}
.dark {
opacity: 50%;
}
</style>

View File

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

View File

@ -0,0 +1,10 @@
var AspectRatio = Object.freeze({
Initial: -1,
Reset: 0,
Automatic: 1,
FitWidth: 2,
FitHeight: 3,
Fixed: 4,
});
export default AspectRatio;

View File

@ -0,0 +1,10 @@
var ExtensionMode = Object.freeze({
AutoDisabled: -2,
Disabled: -1,
Default: 0,
Whitelist: 1,
Basic: 2,
Enabled: 3,
});
export default ExtensionMode;

View File

@ -0,0 +1,9 @@
var Stretch = Object.freeze({
NoStretch: 0,
Basic: 1,
Hybrid: 2,
Conditional: 3,
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

@ -0,0 +1,26 @@
class KeyboardShortcutParser {
static parseShortcut(keypress) {
var shortcutCombo = '';
if (keypress.ctrlKey) {
shortcutCombo += 'Ctrl + ';
}
if (keypress.shiftKey) {
shortcutCombo += 'Shift + ';
}
if (keypress.metaKey) {
shortcutCombo += 'Meta + ';
}
if (keypress.altKey) {
shortcutCombo += 'Alt + ';
}
if (keypress.key) {
shortcutCombo += keypress.key.toUpperCase();
} else {
shortcutCombo += '<mouse action>'
}
return shortcutCombo;
}
}
export default KeyboardShortcutParser;

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,28 @@
export default {
computed: {
scopeActions: function() {
return this.settings.active.actions.filter(x => 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') || [];
},
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) || [];
}
}
}

212
src/ext/conf/ActionList.js Normal file
View File

@ -0,0 +1,212 @@
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';
var ActionList = {
'set-ar': {
name: 'Set aspect ratio',
args: [{
name: 'Automatic',
arg: AspectRatio.Automatic,
},{
name: 'Fit width',
arg: AspectRatio.FitWidth,
},{
name: 'Fit height',
arg: AspectRatio.FitHeight,
},{
name: 'Reset',
arg: AspectRatio.Reset,
},{
name: 'Manually specify ratio',
arg: AspectRatio.Fixed,
customArg: true,
hintHTML: '',
}],
scopes: {
global: false,
site: false,
page: true,
}
},
'set-stretch': {
name: 'Set stretch',
args: [{
name: 'Normal',
arg: Stretch.NoStretch
},{
name: 'Basic',
arg: Stretch.Basic,
},{
name: 'Hybrid',
arg: Stretch.Hybrid,
},{
name: 'Thin borders',
arg: Stretch.Conditional,
},{
name: 'Default',
arg: Stretch.Default,
scopes: {
site: true
}
}],
scopes: {
global: true,
site: true,
page: true,
}
},
'set-alignment': {
name: 'Set video alignment',
args: [{
name: 'Left',
arg: VideoAlignment.Left,
},{
name: 'Center',
arg: VideoAlignment.Center,
},{
name: 'Right',
arg: VideoAlignment.Right
},{
name: 'Default',
arg: VideoAlignment.Default,
scopes: {
site: true,
}
}],
scopes: {
global: true,
site: true,
page: true,
}
},
'pan': {
name: 'Pan',
args: [{
name: '',
arg: 'toggle'
}],
scopes: {
page: true,
}
},
'toggle-pan': {
name: 'Toggle panning mode',
args: [{
name: 'Toggle',
arg: 'toggle',
},{
name: 'Enable',
arg: 'enable',
},{
name: 'Disable',
arg: 'disable'
}],
scopes: {
page: true
}
},
'change-zoom': {
name: 'Zoom',
args: [{
name: 'Zoom level increase/decrease',
customArg: true,
hintHTML: '<small>Positive values zoom in, negative values zoom out. Increases/decreases are logarithmic: value of \'1\' will double the zoom, value of \'-1\' will halve it.</small>'
}],
scopes: {
page: true,
}
},
'set-zoom': {
name: 'Set zoom level',
args: [{
name: 'Zoom level increase/decrease',
customArg: true,
hintHTML: '<small>Examples: 0.5 sets zoom to 50%, 1 sets zoom to 100%, 2 sets zoom to 200%. Don\'t use negative values unless you want to experience Australian youtube.</small>'
}],
scopes: {
page: true,
}
},
'set-extension-mode': {
name: 'Set extension mode',
args: [{
name: 'Enable',
arg: ExtensionMode.Enabled,
},{
name: 'On whitelisted only',
arg: ExtensionMode.Whitelist,
scopes: {
global: true,
}
},{
name: 'Default',
arg: ExtensionMode.Default,
scopes: {
page: true,
}
},{
name: 'Disable',
arg: ExtensionMode.Default
}],
scopes: {
global: true,
site: true,
}
},
'set-autoar-mode': {
name: 'Set automatic aspect ratio detection mode',
args: [{
name: 'Enable',
arg: ExtensionMode.Enabled,
},{
name: 'On whitelisted only',
arg: ExtensionMode.Whitelist,
scopes: {
global: true,
}
},{
name: 'Default',
arg: ExtensionMode.Default,
scopes: {
page: true,
}
},{
name: 'Disable',
arg: ExtensionMode.Disabled
}],
scopes: {
global: true,
site: true,
}
},
'set-keyboard': {
name: 'Keyboard shortcuts',
args: [{
name: 'Enable',
arg: ExtensionMode.Enabled,
},{
name: 'On whitelisted only',
arg: ExtensionMode.Whitelist,
scopes: {
global: true,
}
},{
name: 'Default',
arg: ExtensionMode.Default,
scopes: {
page: true,
}
},{
name: 'Disable',
arg: ExtensionMode.Disabled
}],
scopes: {
global: true,
site: true,
}
}
};
export default ActionList;

View File

@ -0,0 +1,13 @@
import Debug from './Debug.js';
const BrowserDetect = {
firefox: process.env.BROWSER === 'firefox',
chrome: process.env.BROWSER === 'chrome',
edge: process.env.BROWSER === 'edge',
}
if (Debug.debug) {
console.log("Loading: BrowserDetect.js\n\nprocess.env.BROWSER:", process.env.BROWSER, "Exporting BrowserDetect:", BrowserDetect);
}
export default BrowserDetect;

61
src/ext/conf/Debug.js Normal file
View File

@ -0,0 +1,61 @@
// Set prod to true when releasing
const _prod = true;
// const _prod = false;
var Debug = {
// performanceMetrics: true, // should not be affected by debug.debug in order to allow benchmarking of the impact logging in console has
// init: true,
// debug: true,
// debug: false,
// keyboard: true,
// debugResizer: true,
// debugArDetect: true,
// scaler: true,
// debugStorage: false,
// debugStorage: true,
// comms: false,
// comms: true,
// showArDetectCanvas: true,
// flushStoredSettings: true,
// flushStoredSettings: false,
// playerDetectDebug: true,
// periodic: true,
// videoRescan: true,
// mousemove: true,
// arDetect: {
// edgeDetect: true
// },
// canvas: {
// debugDetection: true
// },
debugCanvas: {
// enabled: true,
// guardLine: true
// enabled: false,
// guardLine: false
}
}
if(_prod){
__disableAllDebug(Debug);
}
function __disableAllDebug(obj) {
for(let key in obj) {
if (obj.hasOwnProperty(key) ){
if(obj[key] instanceof Object) {
__disableAllDebug(obj[key]);
}
else {
obj[key] = false;
}
}
}
}
if(Debug.debug)
console.log("Guess we're debugging ultrawidify then. Debug.js must always load first, and others must follow.\nLoading: Debug.js");
export default Debug;

View File

@ -0,0 +1,925 @@
import Debug from './Debug';
import currentBrowser from './BrowserDetect';
import VideoAlignment from '../../common/enums/video-alignment.enum';
import Stretch from '../../common/enums/stretch.enum';
import ExtensionMode from '../../common/enums/extension-mode.enum';
import AntiGradientMode from '../../common/enums/anti-gradient-mode.enum';
import AspectRatio from '../../common/enums/aspect-ratio.enum';
if(Debug.debug)
console.log("Loading: ExtensionConf.js");
var ExtensionConf = {
arDetect: {
disabledReason: "", // if automatic aspect ratio has been disabled, show reason
allowedMisaligned: 0.05, // top and bottom letterbox thickness can differ by this much.
// Any more and we don't adjust ar.
allowedArVariance: 0.075, // amount by which old ar can differ from the new (1 = 100%)
timers: { // autodetection frequency
playing: 333, // while playing
paused: 3000, // while paused
error: 3000, // after error
minimumTimeout: 5,
tickrate: 10, // 1 tick every this many milliseconds
},
autoDisable: { // settings for automatically disabling the extension
maxExecutionTime: 6000, // if execution time of main autodetect loop exceeds this many milliseconds,
// we disable it.
consecutiveTimeoutCount: 5, // we only do it if it happens this many consecutive times
// FOR FUTURE USE
consecutiveArResets: 5 // if aspect ratio reverts immediately after AR change is applied, we disable everything
},
canvasDimensions: {
blackframeCanvas: { // smaller than sample canvas, blackframe canvas is used to recon for black frames
// it's not used to detect aspect ratio by itself, so it can be tiny af
width: 16,
height: 9,
},
sampleCanvas: { // size of image sample for detecting aspect ratio. Bigger size means more accurate results,
// at the expense of performance
width: 640,
height: 360,
},
},
// samplingInterval: 10, // we sample at columns at (width/this) * [ 1 .. this - 1]
blackframe: {
sufficientColorVariance: 0.10, // calculate difference between average intensity and pixel, for every pixel for every color
// component. Average intensity is normalized to where 0 is black and 1 is biggest value for
// that component. If sum of differences between normalized average intensity and normalized
// component varies more than this % between color components, we can afford to use less strict
// cummulative treshold.
cumulativeThresholdLax: 1600,
cumulativeThresholdStrict: 2560,// if we add values of all pixels together and get more than this, the frame is bright enough.
// (note: blackframe is 16x9 px -> 144px total. cumulative threshold can be reached fast)
blackPixelsCondition: 0.6, // How much pixels must be black (1 all, 0 none) before we consider frame as black. Takes
// precedence over cumulative threshold: if blackPixelsCondition is met, the frame is dark
// regardless of whether cumulative threshold has been reached.
},
blackbar: {
blackLevel: 10, // everything darker than 10/255 across all RGB components is considered black by
// default. blackLevel can decrease if we detect darker black.
threshold: 16, // if pixel is darker than the sum of black level and this value, we count it as black
// on 0-255. Needs to be fairly high (8 might not cut it) due to compression
// artifacts in the video itself
frameThreshold: 4, // treshold, but when doing blackframe test
imageThreshold: 16, // in order to detect pixel as "not black", the pixel must be brighter than
// the sum of black level, threshold and this value.
gradientThreshold: 2, // When trying to determine thickness of the black bars, we take 2 values: position of
// the last pixel that's darker than our threshold, and position of the first pixel that's
// brighter than our image threshold. If positions are more than this many pixels apart,
// we assume we aren't looking at letterbox and thus don't correct the aspect ratio.
gradientSampleSize: 16, // How far do we look to find the gradient
maxGradient: 6, // if two neighbouring pixels in gradientSampleSize differ by more than this, then we aren't
// looking at a gradient
gradientNegativeTreshold: -2,
gradientMaxSD: 6, // reserved for future use
antiGradientMode: AntiGradientMode.Lax,
},
variableBlackbarThresholdOptions: { // In case of poor bitrate videos, jpeg artifacts may cause us issues
// FOR FUTURE USE
enabled: true, // allow increasing blackbar threshold
disableArDetectOnMax: true, // disable autodetection when threshold goes over max blackbar threshold
maxBlackbarThreshold: 48, // max threshold (don't increase past this)
thresholdStep: 8, // when failing to set aspect ratio, increase threshold by this much
increaseAfterConsecutiveResets: 2 // increase if AR resets this many times in a row
},
sampling: {
staticCols: 9, // we take a column at [0-n]/n-th parts along the width and sample it
randomCols: 0, // we add this many randomly selected columns to the static columns
staticRows: 9, // forms grid with staticSampleCols. Determined in the same way. For black frame checks
},
guardLine: { // all pixels on the guardline need to be black, or else we trigger AR recalculation
// (if AR fails to be recalculated, we reset AR)
enabled: true,
ignoreEdgeMargin: 0.20, // we ignore anything that pokes over the black line this close to the edge
// (relative to width of the sample)
imageTestThreshold: 0.1, // when testing for image, this much pixels must be over blackbarThreshold
edgeTolerancePx: 2, // black edge violation is performed this far from reported 'last black pixel'
edgeTolerancePercent: null // unused. same as above, except use % of canvas height instead of pixels
},
fallbackMode: {
enabled: true,
safetyBorderPx: 5, // determines the thickness of safety border in fallback mode
noTriggerZonePx: 8 // if we detect edge less than this many pixels thick, we don't correct.
},
arSwitchLimiter: { // to be implemented
switches: 2, // we can switch this many times
period: 2.0 // per this period
},
edgeDetection: {
sampleWidth: 8, // we take a sample this wide for edge detection
detectionThreshold: 4, // sample needs to have this many non-black pixels to be a valid edge
confirmationThreshold: 1, //
singleSideConfirmationThreshold: 3, // 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: 0.15, // if edge candidate sits with count greater than this*all_samples, it can't be logo
// or watermark.
edgeTolerancePx: 1, // we check for black edge violation this far from detection point
edgeTolerancePercent: null, // we check for black edge detection this % of height from detection point. unused
middleIgnoredArea: 0.2, // we ignore this % of canvas height towards edges while detecting aspect ratios
minColsForSearch: 0.5, // if we hit the edge of blackbars for all but this many columns (%-wise), we don't
// continue with search. It's pointless, because black edge is higher/lower than we
// are now. (NOTE: keep this less than 1 in case we implement logo detection)
},
pillarTest: {
ignoreThinPillarsPx: 5, // ignore pillars that are less than this many pixels thick.
allowMisaligned: 0.05 // left and right edge can vary this much (%)
},
textLineTest: {
nonTextPulse: 0.10, // 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: 10, // this is a threshold to confirm we're seeing text.
pulsesToConfirmIfHalfBlack: 5, // this is the threshold to confirm we're seeing text if longest black pulse
// is over 50% of the canvas width
testRowOffset: 0.02 // we test this % of height from detected edge
}
},
zoom: {
minLogZoom: -1,
maxLogZoom: 3,
announceDebounce: 200 // we wait this long before announcing new zoom
},
miscSettings: {
mousePan: {
enabled: false
},
mousePanReverseMouse: false,
},
stretch: {
conditionalDifferencePercent: 0.05 // black bars less than this wide will trigger stretch
// if mode is set to '1'. 1.0=100%
},
resizer: {
setStyleString: {
maxRetries: 3,
retryTimeout: 200
}
},
pageInfo: {
timeouts: {
urlCheck: 200,
rescan: 1500
}
},
// -----------------------------------------
// ::: ACTIONS :::
// -----------------------------------------
// Nastavitve za ukaze. Zamenja stare nastavitve za bližnične tipke.
//
// Polje 'shortcut' je tabela, če se slučajno lotimo kdaj delati choordov.
actions: [{
name: 'Trigger automatic detection', // name displayed in settings
label: 'Automatic', // name displayed in ui (can be overriden in scope/playerUi)
cmd: [{
action: 'set-ar',
arg: AspectRatio.Automatic,
persistent: false, // optional, false by default. If true, change doesn't take effect immediately.
// Instead, this action saves stuff to settings
}],
scopes: {
global: { // if 'global' is undefined, 'show' is presumed to be 'false'
show: false,
},
site: {
show: false,
},
page: {
show: true,
label: 'Automatic', // example override, takes precedence over default label
shortcut: [{
key: 'a',
ctrlKey: false,
metaKey: false,
altKey: false,
shiftKey: false,
onKeyUp: true,
onKeyDown: false,
}],
}
},
playerUi: {
show: true,
path: 'crop',
},
},{
name: 'Reset to default',
label: 'Reset',
cmd: [{
action: 'set-ar',
arg: AspectRatio.Reset,
}],
scopes: {
page: {
show: true,
shortcut: [{
key: 'r',
ctrlKey: false,
metaKey: false,
altKey: false,
shiftKey: false,
onKeyUp: true,
onKeyDown: false,
}],
}
},
playerUi: {
show: true,
path: 'crop'
},
},{
name: 'Fit to width',
label: 'Fit width',
cmd: [{
action: 'set-ar',
arg: AspectRatio.FitWidth,
}],
scopes: {
page: {
show: true,
shortcut: [{
key: 'w',
ctrlKey: false,
metaKey: false,
altKey: false,
shiftKey: false,
onKeyUp: true,
onKeyDown: false,
}],
}
},
playerUi: {
show: true,
path: 'crop'
}
},{
name: 'Fit to height',
label: 'Fit height',
cmd: [{
action: 'set-ar',
arg: AspectRatio.FitHeight
}],
scopes: {
page: {
show: true,
shortcut: [{
key: 'e',
ctrlKey: false,
metaKey: false,
altKey: false,
shiftKey: false,
onKeyUp: true,
onKeyDown: false,
}]
}
},
playerUi: {
show: true,
path: 'crop'
}
},{
name: 'Set aspect ratio to 16:9',
label: '16:9',
cmd: [{
action: 'set-ar',
arg: AspectRatio.Fixed,
customArg: 1.78,
}],
scopes: {
page: {
show: true,
shortcut: [{
key: 's',
ctrlKey: false,
metaKey: false,
altKey: false,
shiftKey: false,
onKeyUp: false,
onKeyDown: true,
}],
}
},
playerUi: {
show: true,
path: 'crop'
}
},{
name: 'Set aspect ratio to 21:9 (2.39:1)',
label: '21:9',
cmd: [{
action: 'set-ar',
arg: AspectRatio.Fixed,
customArg: 2.39
}],
scopes: {
page: {
show: true,
shortcut: [{
key: 'd',
ctrlKey: false,
metaKey: false,
altKey: false,
shiftKey: false,
onKeyUp: false,
onKeyDown: true,
}]
}
},
playerUi: {
show: true,
path: 'crop'
}
},{
name: 'Set aspect ratio to 18:9',
label: '18:9',
cmd: [{
action: 'set-ar',
arg: AspectRatio.Fixed,
customArg: 2.0,
}],
scopes: {
page: {
show: true,
shortcut: [{
key: 'x',
ctrlKey: false,
metaKey: false,
altKey: false,
shiftKey: false,
onKeyUp: true,
onKeyDown: false,
}]
}
},
playerUi: {
show: true,
path: 'crop',
}
},{
name: 'Zoom in',
label: 'Zoom',
cmd: [{
action: 'change-zoom',
arg: 0.1
}],
scopes: {
page: {
show: false,
shortcut: [{
key: 'z',
ctrlKey: false,
metaKey: false,
altKey: false,
shiftKey: false,
onKeyUp: true,
onKeyDown: false,
}],
}
},
playerUi: {
show: false,
}
},{
name: 'Zoom out',
label: 'Unzoom',
cmd: [{
action: 'change-zoom',
arg: -0.1
}],
scopes: {
page: {
show: false,
shortcut: [{
key: 'u',
ctrlKey: false,
metaKey: false,
altKey: false,
shiftKey: false,
onKeyUp: true,
onKeyDown: false,
}],
}
},
playerUi: {
show: false
}
},{
name: 'Toggle panning mode',
label: 'Toggle pan',
cmd: [{
action: 'toggle-pan',
arg: 'toggle'
}],
scopes: {
page: {
show: true,
shortcut: [{
key: 'p',
ctrlKey: false,
metaKey: false,
altKey: false,
shiftKey: false,
onKeyUp: true,
onKeyDown: false,
}]
}
},
playerUi: {
show: true,
path: 'zoom'
}
},{
name: 'Hold to pan',
cmd: [{
action: 'pan',
arg: 'toggle',
}],
scopes: {
page: {
show: false,
shortcut: [{
ctrlKey: false,
metaKey: false,
altKey: false,
shiftKey: true,
onKeyDown: false,
onKeyUp: false,
onMouseMove: true,
}],
}
}
},
//
// S T R E T C H I N G
//
{
name: 'Set stretch to "none"',
label: 'Don\'t stretch',
cmd: [{
action: 'set-stretch',
arg: Stretch.NoStretch,
}],
scopes: {
global: {
show: true,
label: 'Normal'
},
site: {
show: true,
label: 'Normal'
},
page: {
show: true,
label: 'Normal'
}
},
playerUi: {
show: true,
path: 'stretch'
}
},{
name: 'Set stretch to "basic"',
label: 'Basic stretch',
cmd: [{
action: 'set-stretch',
arg: Stretch.Basic,
}],
scopes: {
global: {
show: true,
label: 'Basic'
},
site: {
show: true,
label: 'Basic'
},
page: {
show: true,
label: 'Basic'
}
},
playerUi: {
show: true,
path: 'stretch'
}
},{
name: 'Set stretch to "hybrid"',
label: 'Hybrid stretch',
cmd: [{
action: 'set-stretch',
arg: Stretch.Hybrid,
}],
scopes: {
global: {
show: true,
label: 'Hybrid'
},
site: {
show: true,
label: 'Hybrid'
},
page: {
show: true,
label: 'Hybrid'
}
},
playerUi: {
show: true,
path: 'stretch'
}
},{
name: 'Stretch only to hide thin borders',
label: 'Thin borders only',
cmd: [{
action: 'set-stretch',
arg: Stretch.Conditional,
}],
scopes: {
global: {
show: true,
label: 'Thin borders'
},
site: {
show: true,
label: 'Thin borders'
},
page: {
show: true,
label: 'Thin borders'
}
},
playerUi: {
show: true,
path: 'stretch'
}
},{
name: 'Set stretch to default value',
label: 'Default',
cmd: [{
action: 'set-stretch',
arg: Stretch.Default,
}],
scopes: {
site: {
show: true,
}
}
},
//
// A L I G N M E N T
//
{
name: 'Align video to the left',
label: 'Left',
cmd: [{
action: 'set-alignment',
arg: VideoAlignment.Left,
}],
scopes: {
global: {
show: true,
},
site: {
show: true,
},
page: {
show: true,
}
},
playerUi: {
show: true,
path: 'align'
}
},{
name: 'Align video to center',
label: 'Center',
cmd: [{
action: 'set-alignment',
arg: VideoAlignment.Center,
}],
scopes: {
global: {
show: true,
},
site: {
show: true,
},
page: {
show: true,
}
},
playerUi: {
show: true,
path: 'align'
}
},{
name: 'Align video to the right',
label: 'Right',
cmd: [{
action: 'set-alignment',
arg: VideoAlignment.Right
}],
scopes: {
global: {
show: true,
},
site: {
show: true,
},
page: {
show: true,
}
},
playerUi: {
show: true,
path: 'align'
}
},{
name: 'Use default alignment',
label: 'Default',
cmd: [{
action: 'set-alignment',
arg: VideoAlignment.Default
}],
scopes: {
site: {
show: true,
}
}
},
//
// E N A B L E E X T E N S I O N / A U T O A R
// (for sites/extension tab in the popup)
//
{
name: 'Enable extension',
label: 'Enable',
cmd: [{
action: 'set-extension-mode',
arg: ExtensionMode.Enabled,
persistent: true,
}],
scopes: {
global: {
show: true,
},
site: {
show: true,
}
}
},{
name: 'Enable extension on whitelisted sites only',
label: 'On whitelist only',
cmd: [{
action: 'set-extension-mode',
arg: ExtensionMode.Whitelist,
persistent: true,
}],
scopes: {
global: {
show: true
}
}
},{
name: 'Extension mode: use default settings',
label: 'Default',
cmd: [{
action: 'set-extension-mode',
arg: ExtensionMode.Default,
persistent: true,
}],
scopes: {
site: {
show: true
}
}
},{
name: 'Disable extension',
label: 'Disable',
cmd: [{
action: 'set-extension-mode',
arg: ExtensionMode.Disabled,
persistent: true,
}],
scopes: {
global: {
show: true,
},
site: {
show: true,
}
}
},{
name: 'Enable automatic aspect ratio detection',
label: 'Enable',
cmd: [{
action: 'set-autoar-mode',
arg: ExtensionMode.Enabled,
persistent: true,
}],
scopes: {
global: {
show: true
},
site: {
show: true
}
}
},{
name: 'Enable automatic aspect ratio detection on whitelisted sites only',
label: 'On whitelist only',
cmd: [{
action: 'set-autoar-mode',
arg: ExtensionMode.Whitelist,
persistent: true,
}],
scopes: {
global: {
show: true,
}
}
},{
name: 'Use default settings for automatic aspect ratio detection',
label: 'Default',
cmd: [{
action: 'set-autoar-mode',
arg: ExtensionMode.Default,
persistent: true,
}],
scopes: {
site: {
show: true,
}
}
},{
name: 'Disable automatic aspect ratio detection',
label: 'Disable',
cmd: [{
action: 'set-autoar-mode',
arg: ExtensionMode.Disabled,
persistent: true,
}],
scopes: {
global: {
show: true,
},
site: {
show: true,
}
}
},
//
//
// Enable/disable keyboard shortcuts
//
{
name: 'Enable keyboard shortcuts',
label: 'Enable',
cmd: [{
action: 'set-keyboard',
arg: ExtensionMode.Enabled,
}],
scopes: {
global: {
show: true,
},
site: {
show: true,
},
page: {
show: true,
}
}
},{
name: 'Enable keyboard shortcuts on whitelisted sites only',
label: 'On whitelist only',
cmd: [{
action: 'set-keyboard',
arg: ExtensionMode.Whitelist,
}],
scopes: {
global: {
show: true
},
}
},{
name: 'Keyboard shortcuts mode: use default settings',
label: 'Default',
cmd: [{
action: 'set-keyboard',
arg: ExtensionMode.Default,
}],
scopes: {
site: {
show: true
}
}
},{
name: 'Disable keyboard shortcuts',
label: 'Disable',
cmd: [{
action: 'set-keyboard',
arg: ExtensionMode.Disabled,
}],
scopes: {
global: {
show: true,
},
site: {
show: true,
},
page: {
show: true,
}
}
},
],
// -----------------------------------------
// ::: SITE CONFIGURATION :::
// -----------------------------------------
// 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?
//
// 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
// }
//
// Veljavne vrednosti za možnosti
// 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: {
"@global": { // global defaults. Possible options will state site-only options in order
// to avoid writing this multiple times. Tags:
// #g — only available in @global
// #s — only available for specific site
mode: ExtensionMode.Enabled, // How should extension work:
// 'enabled' - work everywhere except blacklist
// 'whitelist' - only work on whitelisted sites (#g)
// 'disabled' - work nowhere
// 'basic' - (Possible future use)
// 'default' - follow global rules (#s)
autoar: ExtensionMode.Enabled, // Should we try to automatically detect aspect ratio?
// Options: 'enabled', 'whitelist' (#g), 'default' (#s), 'disabled'
autoarFallback: currentBrowser.firefox ? // if autoAr fails, try fallback mode?
ExtensionMode.Enabled : // Options same as in autoar.
ExtensionMode.Disabled, // if autoar is disabled, this setting is irrelevant
stretch: Stretch.NoStretch, // Default stretch mode.
videoAlignment: VideoAlignment.Center, // Video alignment
keyboardShortcutsEnabled: ExtensionMode.Enabled,
},
"www.youtube.com" : {
mode: ExtensionMode.Enabled,
autoar: ExtensionMode.Enabled,
autoarFallback: ExtensionMode.Enabled,
override: false, // ignore value localStorage in favour of this
type: 'official', // is officially supported? (Alternatives are 'community' and 'user-defined')
actions: null, // overrides global keyboard shortcuts and button configs. Is array, is optional.
stretch: Stretch.Default,
videoAlignment: VideoAlignment.Default,
keyboardShortcutsEnabled: ExtensionMode.Default,
},
"www.netflix.com" : {
mode: ExtensionMode.Enabled,
autoar: currentBrowser.firefox ? ExtensionMode.Enabled : ExtensionMode.Disabled,
override: false,
type: 'official',
stretch: Stretch.Default,
videoAlignment: VideoAlignment.Default,
keyboardShortcutsEnabled: ExtensionMode.Default,
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 runing if <video> had a 'width' property in style, regardless of value
// could also be an empty object, in theory.
}
}
}
},
}
}
export default ExtensionConf;

View File

@ -0,0 +1,285 @@
import Debug from '../conf/Debug';
import PlayerData from './video-data/PlayerData';
import ExtensionMode from '../../common/enums/extension-mode.enum';
class ActionHandler {
constructor(pageInfo) {
this.pageInfo = pageInfo;
this.settings = pageInfo.settings;
this.inputs = ['input', 'select', 'button', 'textarea'];
this.keyboardLocalDisabled = false;
}
init() {
if (Debug.debug) {
console.log("[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.host].actions) {
actions = this.settings.active.sites[window.location.host].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,
});
}
}
}
document.addEventListener('keydown', (event) => ths.handleKeydown(event) );
document.addEventListener('keyup', (event) => ths.handleKeyup(event) );
this.pageInfo.setActionHandler(this);
if (Debug.debug) {
console.log("[ActionHandler::init] initialization complete");
}
}
registerHandleMouse(videoData) {
if (Debug.debug && Debug.mousemove) {
console.log("[ActionHandler::registerHandleMouse] registering handle mouse for videodata:", videoData)
}
var ths = this;
if (videoData.player && videoData.player.element) {
videoData.player.element.addEventListener('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() {
var activeElement = document.activeElement;
if(Debug.debug && Debug.keyboard) {
Debug.debug = false; // temp disable to avoid recursing;
console.log("[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",
"\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)", this.preventAction(),
"\n-----------------{ extra debug info }-------------------",
"\ntag name? (lowercase):", activeElement.tagName, activeElement.tagName.toLocaleLowerCase(),
"\nrole:", activeElement.getAttribute('role'),
"\ntype:", activeElement.getAttribute('type'),
"insta-fail inputs:", this.inputs
);
Debug.debug = true; // undisable
}
// 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 (activeElement.getAttribute("type") === "text") {
return true;
}
return false;
}
isActionMatch(shortcut, event) {
return shortcut.key === event.key &&
shortcut.ctrlKey === event.ctrlKey &&
shortcut.metaKey === event.metaKey &&
shortcut.altKey === event.altKey &&
shortcut.shiftKey === event.shiftKey
}
execAction(actions, event, videoData) {
if(Debug.debug && Debug.keyboard ){
console.log("%c[ActionHandler::execAction] Trying to find and execute action for event. Actions/event: ", "color: #ff0", actions, event);
}
for (var action of actions) {
if (this.isActionMatch(action.shortcut, event)) {
if(Debug.debug && Debug.keyboard ){
console.log("%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.host : '@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;
}
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) {
if(Debug.debug && Debug.keyboard ){
console.log("%c[ActionHandler::handleKeyup] we pressed a key: ", "color: #ff0", event.key , " | keyup: ", event.keyup, "event:", event);
}
if (this.preventAction()) {
if (Debug.debug && Debug.keyboard) {
console.log("[ActionHandler::handleKeyup] we are in a text box or something. Doing nothing.");
}
return;
}
this.execAction(this.keyUpActions, event);
}
handleKeydown(event) {
if(Debug.debug && Debug.keyboard ){
console.log("%c[ActionHandler::handleKeydown] we pressed a key: ", "color: #ff0", event.key , " | keydown: ", event.keydown, "event:", event);
}
if (this.preventAction()) {
if (Debug.debug && Debug.keyboard) {
console.log("[ActionHandler::handleKeydown] we are in a text box or something. Doing nothing.");
}
return;
}
this.execAction(this.keyDownActions, event);
}
handleMouseMove(event, videoData) {
if (Debug.debug && Debug.mousemove) {
console.log("[ActionHandler::handleMouseMove] mouse move is being handled.\nevent:", event, "\nvideo data:", videoData);
}
videoData.panHandler(event);
this.execAction(this.mouseMoveActions, event, videoData)
}
}
export default ActionHandler;

View File

@ -1,3 +1,5 @@
import Debug from '../conf/Debug';
class ObjectCopy {
static addNew(existing, target){
@ -50,4 +52,6 @@ class ObjectCopy {
// target: object that we want
// ignoreKeys: if key is an object, we don't recursively call this function on that key
}
}
}
export default ObjectCopy;

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

@ -0,0 +1,418 @@
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';
class Settings {
constructor(activeSettings, updateCallback) {
this.active = activeSettings ? activeSettings : undefined;
this.default = ExtensionConf;
this.useSync = false;
this.version = undefined;
this.updateCallback = updateCallback;
const ths = this;
if(currentBrowser.firefox) {
browser.storage.onChanged.addListener( (changes, area) => {
if (Debug.debug && Debug.debugStorage) {
console.log("[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) {
console.log("[Settings::<storage/on change>] new settings object:", JSON.parse(changes.uwSettings.newValue));
}
}
if(changes['uwSettings'] && changes['uwSettings'].newValue) {
ths.setActive(JSON.parse(changes.uwSettings.newValue));
}
if(this.updateCallback) {
try {
updateCallback(ths);
} catch (e) {
console.log("[Settings] CALLING UPDATE CALLBACK FAILED.")
}
}
});
} else if (currentBrowser.chrome) {
chrome.storage.onChanged.addListener( (changes, area) => {
if (Debug.debug && Debug.debugStorage) {
console.log("[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) {
console.log("[Settings::<storage/on change>] new settings object:", JSON.parse(changes.uwSettings.newValue));
}
}
if(changes['uwSettings'] && changes['uwSettings'].newValue) {
ths.setActive(JSON.parse(changes.uwSettings.newValue));
}
if(this.updateCallback) {
try {
updateCallback(ths);
} catch (e) {
console.log("[Settings] CALLING UPDATE CALLBACK FAILED.")
}
}
});
}
}
async init() {
const settings = await this.get();
if(Debug.debug) {
console.log("[Settings::init] Configuration fetched from storage:", settings);
if (Debug.flushStoredSettings) {
console.log("%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.setDefaultSettings();
this.active = this.getDefaultSettings();
return this.active;
}
}
// if there's no settings saved, return default settings.
if(! settings || (Object.keys(settings).length === 0 && settings.constructor === Object)) {
this.setDefaultSettings();
this.active = this.getDefaultSettings();
return this.active;
}
// 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 (settings.version && !settings.version.startsWith('4')) {
this.setDefaultSettings();
this.active = this.getDefaultSettings();
return this.active;
}
// if there's settings, set saved object as active settings
this.active = settings;
// check if extension has been updated. If not, return settings as they were retreived
if (currentBrowser.firefox) {
this.version = browser.runtime.getManifest().version;
} else if (currentBrowser.chrome) {
this.version = chrome.runtime.getManifest().version;
} else if (currentBrowser.edge) {
this.version = browser.runtime.getManifest().version;
}
if(settings.version === this.version) {
if(Debug.debug) {
console.log("[Settings::init] extension was saved with current version of ultrawidify (", this.version, "). Returning object as-is.");
}
return this.active;
}
// 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);
if(Debug.debug) {
console.log("[Settings.init] Results from ObjectCopy.addNew()?", patched, "\n\nSettings from storage", settings, "\ndefault?", this.default,);
}
if(patched){
this.active = patched;
} else {
this.active = JSON.parse(JSON.stringify(this.default));
}
this.set(this.active);
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));
});
}
try {
return JSON.parse(ret.uwSettings);
} catch(e) {
return undefined;
}
}
async set(extensionConf) {
if (Debug.debug) {
console.log("[Settings::set] setting new settings:", extensionConf)
}
if (currentBrowser.firefox || currentBrowser.edge) {
extensionConf.version = this.version;
return this.useSync ? browser.storage.local.set( {'uwSettings': JSON.stringify(extensionConf)}): browser.storage.local.set( {'uwSettings': JSON.stringify(extensionConf)});
} else if (currentBrowser.chrome) {
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() {
if (Debug.debug) {
console.log("[Settings::save] Saving active settings:", this.active);
}
this.set(this.active);
}
async rollback() {
this.active = await this.get();
}
getDefaultSettings() {
return JSON.parse(JSON.stringify(this.default));
}
setDefaultSettings() {
this.set(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) {
console.log("[Settings::canStartExtension] window.location.hostname is null or undefined:", window.location.hostname)
console.log("active 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.Default && site !== '@global') {
return this.getExtensionMode('@global');
} else {
return ExtensionMode.Disabled;
}
} catch(e){
if(Debug.debug){
console.log("[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) {
console.log("[Settings::canStartExtension] window.location.hostname is null or undefined:", window.location.hostname)
console.log("active 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]) {
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){
if(Debug.debug){
console.log("[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) {
if (Debug.debug) {
console.error("[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.host;
if (!site) {
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;
console.log("[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]) {
return this.active.sites['@global'].autoar === ExtensionMode.Enabled;
}
if (this.active.sites['@global'].autoar === ExtensionMode.Enabled) {
return this.active.sites[site].autoar !== ExtensionMode.Disabled;
} else if (this.active.sites['@global'].autoar === ExtensionMode.Whitelist) {
console.log("canStartAutoAr — can(not) start csar because extension is in whitelist mode, and this site is (not) equal to", ExtensionMode.Enabled)
return this.active.sites[site].autoar === ExtensionMode.Enabled;
} else {
console.log("canStartAutoAr — cannot start csar 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] && this.active.sites[site].stretch !== Stretch.Default) {
return this.active.sites[site].stretch;
}
return this.active.sites['@global'].stretch;
}
getDefaultVideoAlignment(site) {
if (site && this.active.sites[site] && this.active.sites[site].videoAlignment !== VideoAlignment.Default) {
return this.active.sites[site].videoAlignment;
}
return this.active.sites['@global'].videoAlignment;
}
}
export default Settings;

View File

@ -0,0 +1,981 @@
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';
class ArDetector {
constructor(videoData){
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;
if (Debug.init) {
console.log("[ArDetector::ctor] creating new ArDetector. arid:", this.arid);
}
}
setManualTick(manualTick) {
this._manualTicks = manualTick;
}
tick() {
this._nextTick = true;
}
init(){
if (Debug.debug || Debug.init) {
console.log("[ArDetect::init] Initializing autodetection. arid:", this.arid);
}
try {
if (this.settings.canStartAutoAr()) {
this.setup();
} else {
throw "Settings prevent autoar from starting"
}
} catch (e) {
if (Debug.debug) {
console.log("%c[ArDetect::init] INITIALIZATION FAILED!\n", _ard_console_stop, e);
}
}
}
destroy(){
if(Debug.debug || Debug.init) {
console.log(`[ArDetect::destroy] <arid:${this.arid}>`)
}
// this.debugCanvas.destroy();
this.stop();
}
setup(cwidth, cheight){
if(Debug.debug || Debug.init) {
console.log("[ArDetect::setup] Starting autodetection setup. arid:", this.arid);
}
//
// [-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 ){
if(Debug.debug){
console.log("[ArDetector::setup] video has no width or height!", 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) {
if(Debug.debug) {
console.log("%c[ArDetect::setup] WARNING: CANVAS RESET DETECTED - 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: null});
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 (Debug.debug) {
console.log("%c[ArDetect::setup] Starting automatic aspect ratio detection.", _ard_console_start);
}
if (this.conf.resizer.lastAr.type === AspectRatio.Automatic) {
// ensure first autodetection will run in any case
this.conf.resizer.setLastAr({type: AspectRatio.Automatic, ratio: null});
}
// launch main() if it's currently not running:
this.main();
this._halted = false;
this._paused = false;
}
unpause() {
if(this._paused){ // resume only if we explicitly paused
this.start();
}
}
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(){
if(Debug.debug){
console.log("%c[ArDetect::_ard_stop] Stopping automatic aspect ratio detection", _ard_console_stop);
}
this._halted = true;
// this.conf.resizer.resetLastAr();
}
async main() {
if (this.paused) {
// unpause if paused
this._paused = false;
}
if (!this._halted) {
// we are already running, don't run twice
return;
}
let exitedRetries = 10;
while (!this._exited && exitedRetries --> 0) {
if (Debug.debug) {
console.log("[ArDetector::main] 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 this.sleep(this.settings.active.arDetect.timers.tickrate);
}
if (!this._exited) {
if (Debug.debug) {
console.log("[ArDetector::main] Previous instance didn't exit in time. Not starting a new one.")
}
return;
}
if (Debug.debug) {
console.log("%c[ArDetect::main] Main autodetection loop started.", _ard_console_start);
}
// 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) {
if (Debug.debug) {
console.log("%c[ArDetector::main] 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 this.sleep(this.settings.active.arDetect.timers.tickrate);
}
if (Debug.debug) {
console.log(`%c[ArDetect::main] Main autodetection loop exited. Halted? ${this._halted}`, _ard_console_stop);
}
this._exited = true;
}
async sleep(timeout) {
return new Promise( (resolve, reject) => setTimeout(() => resolve(), timeout));
}
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){console.log("[ArDetector::scheduleInitRestart] Failed to start init(). 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(){
if(Debug.debug)
console.log("%c[ArDetect::_ard_canvasReadyForDrawWindow] (?)", "color: #44f", this.canvas.height == window.innerHeight, "(ard_height:", this.canvas.height, "| window height:", window.innerHeight, ")");
return this.canvas.height == window.innerHeight
}
getTimeout(baseTimeout, startTime){
var execTime = (performance.now() - startTime);
if( execTime > this.settings.active.arDetect.autoDisable.maxExecutionTime ){
// this.detectionTimeoutEventCount++;
if(Debug.debug){
console.log("[ArDetect::getTimeout] Exec time exceeded maximum allowed execution time. This has now happened " + this.detectionTimeoutEventCount + " times in a row.");
}
// if( this.detectionTimeoutEventCount >= this.settings.active.arDetect.autoDisable.consecutiveTimeoutCount ){
// if (Debug.debug){
// console.log("[ArDetect::getTimeout] Maximum execution time was exceeded too many times. Automatic aspect ratio detection has been disabled.");
// }
// Comms.sendToBackgroundScript({cmd: 'disable-autoar', reason: 'Automatic aspect ratio detection was taking too much time and has been automatically disabled in order to avoid lag.'});
// _ard_stop();
// return 999999;
// }
} else {
this.detectionTimeoutEventCount = 0;
}
// return baseTimeout > this.settings.active.arDetect.minimumTimeout ? baseTimeout : this.settings.active.arDetect.minimumTimeout;
return baseTimeout;
}
//#endregion
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 ){
if(Debug.debug && Debug.debugArDetect) {
console.log("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){
let actualHeight = 0; // purely for fallback mode
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){
// 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
if(Debug.debug && Debug.debugArDetect)
console.log("%c[ArDetect::processAr] 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){
if(Debug.debug && Debug.debugArDetect)
console.log("%c[ArDetect::processAr] aspect ratio change denied — diff %:", "background: #740; color: #fa2", arDiff_percent)
return;
}
else if(Debug.debug && Debug.debugArDetect){
console.log("%c[ArDetect::processAr] aspect ratio change accepted — diff %:", "background: #153; color: #4f9", arDiff_percent)
}
}
if(Debug.debug) {
console.log("%c[ArDetect::processAr] Triggering aspect ratio change. New aspect ratio: ", _ard_console_change, trueAr);
}
this.conf.resizer.setAr({type: AspectRatio.Automatic, ratio: trueAr}, {type: AspectRatio.Automatic, ratio: trueAr});
}
frameCheck(){
if(! this.video){
if(Debug.debug || Debug.warnings_critical) {
console.log("[ArDetect::frameCheck] 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);
this.fallbackMode = false;
} catch (e) {
if(Debug.debug && Debug.debugArDetect) {
console.log(`%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.canDoFallbackMode) {
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();
var newCanvasWidth = window.innerHeight * (this.video.videoWidth / this.video.videoHeight);
var 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) {
console.log(`%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)
if (Debug.debug) {
console.log("%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
if (Debug.debug && Debug.arDetect) {
console.log("%c[ArDetect::frameCheck] Black frame analysis suggests this frame is black or too dark. Doing nothing,", "color: #fa3", bfanalysis);
}
return;
} else {
// if (Debug.debug && Debug.arDetect) {
// console.log("%c[ArDetect::frameCheck] Black frame analysis suggests this frame is not completely black. Doing further analysis,", "color: #3fa", bfanalysis);
// }
}
// 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.reset({type: AspectRatio.Automatic, ratio: null});
this.guardLine.reset();
this.noLetterboxCanvasReset = true;
if (Debug.debug && Debug.arDetect) {
console.log("%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");
}
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) {
if(Debug.debug && Debug.debugArDetect){
console.log(`%c[ArDetect::frameCheck] guardLine tests were successful. (no imagefail and no blackbarfail)\n`, "color: #afa", guardLineOut);
}
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.reset({type: AspectRatio.Automatic, ratio: null});
this.guardLine.reset();
this.noLetterboxCanvasReset = true;
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(Debug.debug && guardLineOut.blackbarFail){
console.log("[ArDetect::frameCheck] Detected blackbar violation and pillarbox. Resetting to default aspect ratio.");
}
if(guardLineOut.blackbarFail){
this.conf.resizer.reset({type: AspectRatio.Automatic, ratio: null});
this.guardLine.reset();
}
triggerTimeout = this.getTimeout(baseTimeout, startTime);
this.scheduleFrameCheck(triggerTimeout);
return;
}
}
} catch(e) {
if(Debug.debug) {
console.log("[ArDetect.js::frameCheck] something went wrong when 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);
if(Debug.debug && Debug.debugArDetect){
console.log(`%c[ArDetect::frameCheck] edgeDetector returned this\n`, "color: #aaf", edgePost);
}
// console.log("SAMPLES:", blackbarSamples, "candidates:", edgeCandidates, "post:", edgePost,"\n\nblack level:", this.blackLevel, "tresh:", this.blackLevel + this.settings.active.arDetect.blackbar.threshold);
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
if (Debug.debug && Debug.arDetect) {
console.log("%c[ArDetect::frameCheck] Edge wasn't detected with findBars", "color: #fa3", edgePost, "EdgeStatus.AR_KNOWN:", EdgeStatus.AR_KNOWN);
}
return;
}
var newAr = this.calculateArFromEdges(edgePost);
// if (this.fallbackMode
// && (!guardLineOut.blackbarFail && guardLineOut.imageFail)
// && newAr < this.conf.resizer.getLastAr().ar
// ) {
// // V primeru nesmiselnih rezultatov tudi ne naredimo ničesar.
// // v fallback mode se lahko naredi, da je novo razmerje stranice ožje kot staro, kljub temu da je šel
// // blackbar test skozi. Spremembe v tem primeru ne dovolimo.
// //
// // (Pravilen fix? Popraviti je treba računanje robov. V fallback mode je treba upoštevati, da obrobe,
// // ki smo jih obrezali, izginejo is canvasa)
// //
// // NOTE: pravilen fix je bil implementiran
// return;
// }
if(Debug.debug && Debug.debugArDetect){
console.log(`%c[ArDetect::frameCheck] Triggering aspect ration change! new ar: ${newAr}`, "color: #aaf");
}
this.processAr(newAr);
// we also know edges for guardline, so set them.
// we need to be mindful of fallbackMode though
if (!this.fallbackMode) {
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
})
}
}
// }
// else{
// console.log("detected text on edges, dooing nothing")
// }
}
resetBlackLevel(){
this.blackLevel = this.settings.active.arDetect.blackbar.blackLevel;
}
blackLevelTest_full() {
}
blackframeTest() {
if (this.blackLevel === undefined) {
if (Debug.debug && Debug.debugArDetect) {
console.log("[ArDetect::frameCheck] 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) {
console.log("CURRENT MAX:", currentMax, "BLACK LEVEL, threshold, bl+t", this.blackLevel, blthreshold, 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

@ -116,7 +116,7 @@ class DebugCanvas {
}
DebugCanvasClasses = {
VIOLATION: {color: '#ff0000', colorRgb: [255, 00, 0], text: 'violation (general)'},
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)'},

View File

@ -1,3 +1,5 @@
import Debug from '../../conf/Debug';
class GuardLine {
// ardConf — reference to ArDetector that has current GuardLine instance
@ -51,7 +53,8 @@ class GuardLine {
check(image, fallbackMode){
// izračunaj enkrat in shrani na objekt
// calculate once and save object-instance-wide
this.blackbarTreshold = this.conf.blackLevel + this.settings.active.arDetect.blackbarTreshold;
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
@ -98,7 +101,6 @@ class GuardLine {
var offset = parseInt(this.conf.canvas.width * this.settings.active.arDetect.guardLine.ignoreEdgeMargin) << 2;
var offenders = [];
var firstOffender = -1;
var offenderCount = -1; // doing it this way means first offender has offenderCount==0. Ez index.
// TODO: implement logo check.
@ -161,7 +163,7 @@ class GuardLine {
if(!this.imageBar.top || !this.imageBar.bottom)
return { success: false };
var offset = parseInt(this.conf.canvas.width * this.settings.active.arDetect.guardLine.ignoreEdgeMargin) << 2;
var offset = (this.conf.canvas.width * this.settings.active.arDetect.guardLine.ignoreEdgeMargin) << 2;
// TODO: implement logo check.
@ -182,7 +184,7 @@ class GuardLine {
// 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 successTreshold = parseInt(this.conf.canvas.width * this.settings.active.arDetect.guardLine.imageTestTreshold);
var successThreshold = (this.conf.canvas.width * this.settings.active.arDetect.guardLine.imageTestThreshold);
var rowStart, rowEnd;
@ -194,9 +196,9 @@ class GuardLine {
var res = false;
if(Debug.debugCanvas.enabled && Debug.debugCanvas.guardLine){
res = this._ti_debugCheckRow(image, rowStart, rowEnd, successTreshold);
res = this._ti_debugCheckRow(image, rowStart, rowEnd, successThreshold);
} else {
res = this._ti_checkRow(image, rowStart, rowEnd,successTreshold);
res = this._ti_checkRow(image, rowStart, rowEnd,successThreshold);
}
if(res)
@ -210,9 +212,9 @@ class GuardLine {
if(Debug.debugCanvas.enabled && Debug.debugCanvas.guardLine){
res = this._ti_debugCheckRow(image, rowStart, rowEnd, successTreshold);
res = this._ti_debugCheckRow(image, rowStart, rowEnd, successThreshold);
} else {
res = this._ti_checkRow(image, rowStart, rowEnd,successTreshold);
res = this._ti_checkRow(image, rowStart, rowEnd,successThreshold);
}
return {success: res};
@ -228,7 +230,7 @@ class GuardLine {
// 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.blackbarTreshold || image[i+1] > this.blackbarTreshold || image[i+2] > this.blackbarTreshold){
if(image[i] > this.blackbarThreshold || image[i+1] > this.blackbarThreshold || image[i+2] > this.blackbarThreshold){
if(firstOffender < 0){
firstOffender = (i - rowStart) >> 2;
offenderCount++;
@ -253,7 +255,7 @@ class GuardLine {
// 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.blackbarTreshold || image[i+1] > this.blackbarTreshold || image[i+2] > this.blackbarTreshold){
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;
@ -275,10 +277,10 @@ class GuardLine {
return offenderCount;
}
_ti_checkRow(image, rowStart, rowEnd, successTreshold) {
_ti_checkRow(image, rowStart, rowEnd, successThreshold) {
for(var i = rowStart; i < rowEnd; i+=4){
if(image[i] > this.blackbarTreshold || image[i+1] > this.blackbarTreshold || image[i+2] > this.blackbarTreshold){
if(successTreshold --<= 0){
if(image[i] > this.imageThreshold || image[i+1] > this.imageThreshold || image[i+2] > this.imageThreshold){
if(successThreshold --<= 0){
return true;
}
}
@ -287,11 +289,11 @@ class GuardLine {
return false;
}
_ti_debugCheckRow(image, rowStart, rowEnd, successTreshold) {
_ti_debugCheckRow(image, rowStart, rowEnd, successThreshold) {
for(var i = rowStart; i < rowEnd; i+=4){
if(image[i] > this.blackbarTreshold || image[i+1] > this.blackbarTreshold || image[i+2] > this.blackbarTreshold){
if(image[i] > this.imageThreshold || image[i+1] > this.imageThreshold || image[i+2] > this.imageThreshold){
this.conf.debugCanvas.trace(i, DebugCanvasClasses.GUARDLINE_IMAGE);
if(successTreshold --<= 0){
if(successThreshold --<= 0){
return true;
}
} else {
@ -302,3 +304,5 @@ class GuardLine {
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,35 @@
import Debug from '../../conf/Debug';
import BrowserDetect from '../../conf/BrowserDetect';
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);
}
});
}
}
}
export default Comms;

View File

@ -0,0 +1,185 @@
import Debug from '../../conf/Debug';
import BrowserDetect from '../../conf/BrowserDetect';
class CommsClient {
constructor(name, settings) {
if (BrowserDetect.firefox) {
this.port = browser.runtime.connect({name: name});
} else if (BrowserDetect.chrome) {
this.port = chrome.runtime.connect({name: name});
} else if (BrowserDetect.edge) {
this.port = browser.runtime.connect({name: name})
}
var ths = this;
this._listener = m => ths.processReceivedMessage(m);
this.port.onMessage.addListener(this._listener);
this.settings = settings;
this.pageInfo = undefined;
this.commsId = (Math.random() * 20).toFixed(0);
}
destroy() {
this.pageInfo = null;
this.settings = null;
if (!BrowserDetect.edge) { // edge is a very special browser made by outright morons.
this.port.onMessage.removeListener(this._listener);
}
}
setPageInfo(pageInfo){
this.pageInfo = pageInfo;
if(Debug.debug) {
console.log(`[CommsClient::setPageInfo] <${this.commsId}>`, "SETTING PAGEINFO —", this.pageInfo, this)
}
var ths = this;
this._listener = m => ths.processReceivedMessage(m);
if (!BrowserDetect.edge) {
this.port.onMessage.removeListener(this._listener);
}
this.port.onMessage.addListener(this._listener);
}
processReceivedMessage(message){
if(Debug.debug && Debug.comms){
console.log(`[CommsClient.js::processMessage] <${this.commsId}> Received message from background script!`, message);
}
if (!this.pageInfo || !this.settings.active) {
if(Debug.debug && Debug.comms){
console.log(`[CommsClient.js::processMessage] <${this.commsId}> this.pageInfo (or settings) not defined. Extension is probably disabled for this site.\npageInfo:`, this.pageInfo,
"\nsettings.active:", this.settings.active,
"\nnobj:", this
);
}
return;
}
if (message.cmd === 'get-current-zoom') {
this.pageInfo.requestCurrentZoom();
}
if (message.cmd === "set-ar") {
this.pageInfo.setAr({type: message.arg, ratio: message.customArg}, message.playing);
} else if (message.cmd === 'set-alignment') {
this.pageInfo.setvideoAlignment(message.arg, message.playing);
this.pageInfo.restoreAr();
} else if (message.cmd === "set-stretch") {
this.pageInfo.setStretchMode(message.arg, message.playing);
} else if (message.cmd === 'set-keyboard') {
this.pageInfo.setKeyboardShortcutsEnabled(message.arg)
} else if (message.cmd === "autoar-start") {
if (message.enabled !== false) {
this.pageInfo.initArDetection(message.playing);
this.pageInfo.startArDetection(message.playing);
} else {
this.pageInfo.stopArDetection(message.playing);
}
} else if (message.cmd === "pause-processing") {
this.pageInfo.pauseProcessing(message.playing);
} else if (message.cmd === "resume-processing") {
// todo: autoArStatus
this.pageInfo.resumeProcessing(message.autoArStatus, message.playing);
} else if (message.cmd === 'set-zoom') {
this.pageInfo.setZoom(message.arg, true, message.playing);
} else if (message.cmd === 'change-zoom') {
this.pageInfo.zoomStep(message.arg, message.playing);
} else if (message.cmd === 'mark-player') {
this.pageInfo.markPlayer(message.name, message.color);
} else if (message.cmd === 'unmark-player') {
this.pageInfo.unmarkPlayer();
} else if (message.cmd === 'autoar-set-manual-tick') {
this.pageInfo.setManualTick(message.arg);
} else if (message.cmd === 'autoar-tick') {
this.pageInfo.tick();
}
}
async sleep(n){
return new Promise( (resolve, reject) => setTimeout(resolve, n) );
}
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(){
if(Debug.debug){
console.log("%c[CommsClient::requestSettings] sending request for congif!", "background: #11D; color: #aad");
}
var response = await this.sendMessage_nonpersistent({cmd: 'get-config'});
if(Debug.debug){
console.log("%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);
}
registerVideo(){
if (Debug.debug && Debug.comms) {
console.log(`[CommsClient::registerVideo] <${this.commsId}>`, "Registering video for current page.");
}
if (this.pageInfo) {
if (this.pageInfo.videos.length) {
this.port.postMessage({cmd: "has-video"});
}
} else {
// this.port.postMessage({cmd: "has-video"});
}
}
sendPerformanceUpdate(message){
this.port.postMessage({cmd: 'performance-update', message: message});
}
unregisterVideo(){
if (Debug.debug && Debug.comms) {
console.log(`[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()
}
}
export default CommsClient;

View File

@ -0,0 +1,272 @@
import Debug from '../../conf/Debug';
import BrowserDetect from '../../conf/BrowserDetect';
class CommsServer {
constructor(server) {
this.server = server;
this.settings = server.settings;
this.ports = [];
var ths = this;
// console.log("[CommsServer::ctor] INIT! are we in ff?", BrowserDetect.firefox, "BrowserDetect says ...", BrowserDetect)
if (BrowserDetect.firefox) {
browser.runtime.onConnect.addListener(p => ths.onConnect(p));
browser.runtime.onMessage.addListener(m => ths.processReceivedMessage_nonpersistent(m));
} else {
chrome.runtime.onConnect.addListener(p => ths.onConnect(p));
chrome.runtime.onMessage.addListener((m, sender, callback) => ths.processReceivedMessage_nonpersistent(m, sender, callback));
}
}
async toObject(obj) {
// console.log("(not actually) CLONING OBJECT", obj);
// try {
// const r = JSON.parse(JSON.stringify(obj));
// return r;
// } catch (e) {
// console.log("ERROR WHILE CLONING", obj);
return obj;
// }
}
async getCurrentTabHostname() {
const activeTab = await this._getActiveTab();
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){
message = JSON.parse(JSON.stringify(message)); // vue quirk. We should really use vue store instead
for(var p of this.ports){
for(var frame in p){
p[frame].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;
});
});
}
}
async sendToFrame(message, tab, frame) {
// message = JSON.parse(JSON.stringify(message)); // vue quirk. We should really use vue store instead
if(Debug.debug && Debug.comms){
console.log(`%c[CommsServer::sendToFrame] attempting to send message to tab ${tab}, frame ${frame}`, "background: #dda; color: #11D", message);
}
if (isNaN(tab)) {
if (tab === '__playing') {
message['playing'] = true;
this.sendToAll(message);
return;
} else if (tab === '__all') {
this.sendToAll(message);
return;
}
[tab, frame] = tab.split('-')
}
if(Debug.debug && Debug.comms){
console.log(`%c[CommsServer::sendToFrame] attempting to send message to tab ${tab}, frame ${frame}`, "background: #dda; color: #11D", message);
}
try {
this.ports[tab][frame].postMessage(message);
} catch (e) {
if(Debug.debug && Debug.comms){
console.log(`%c[CommsServer::sendToFrame] Sending message failed. Reason:`, "background: #dda; color: #11D", e);
}
}
}
async sendToActive(message) {
message = JSON.parse(JSON.stringify(message)); // vue quirk. We should really use vue store instead
if(Debug.debug && Debug.comms){
console.log("%c[CommsServer::sendToActive] trying to send a message to active tab. Message:", "background: #dda; color: #11D", message);
}
var tabs = await this._getActiveTab();
if(Debug.debug && Debug.comms){
console.log("[CommsServer::_sendToActive] currently active tab(s)?", tabs);
for (var key in this.ports[tabs[0].id]) {
console.log("key?", key, this.ports[tabs[0].id]);
}
}
for (var key in this.ports[tabs[0].id]) {
this.ports[tabs[0].id][key].postMessage(message);
}
}
onConnect(port){
var ths = this;
// poseben primer | special case
if (port.name === 'popup-port') {
this.popupPort = port;
this.popupPort.onMessage.addListener( (m,p) => ths.processReceivedMessage(m,p));
return;
}
var tabId = port.sender.tab.id;
var frameId = port.sender.frameId;
if(! this.ports[tabId]){
this.ports[tabId] = {};
}
this.ports[tabId][frameId] = port;
this.ports[tabId][frameId].onMessage.addListener( (m,p) => ths.processReceivedMessage(m, p));
this.ports[tabId][frameId].onDisconnect.addListener( (p) => {
delete ths.ports[p.sender.tab.id][p.sender.frameId];
if(Object.keys(ths.ports[p.sender.tab.id]).length === 0){
ths.ports[tabId] = undefined;
}
});
}
async processReceivedMessage(message, port){
if (Debug.debug && Debug.comms) {
console.log("[CommsServer.js::processReceivedMessage] Received message from popup/content script!", message, "port", port, "\nsettings and server:", this.settings,this.server);
}
if (message.forwardToContentScript) {
if (Debug.debug && Debug.comms) {
console.log("[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) {
if (Debug.debug && Debug.comms) {
console.log("[CommsServer.js::processReceivedMessage] Message has 'forward to all' flag set. Forwarding message as is. Message:", message);
}
this.sendToAll(message);
}
if (message.forwardToActive) {
if (Debug.debug && Debug.comms) {
console.log("[CommsServer.js::processReceivedMessage] Message has 'forward to active' flag set. Forwarding message as is. Message:", message);
}
this.sendToActive(message)
}
if (message.cmd === 'announce-zoom') {
// forward off to the popup, no use for this here
try {
this.popupPort.postMessage({cmd: 'set-current-zoom', zoom: message.zoom});
} catch (e) {
// can't forward stuff to popup if it isn't open
}
} else if (message.cmd === 'get-current-zoom') {
this.sendToActive(message);
}
if (message.cmd === 'get-current-site') {
port.postMessage({
cmd: 'set-current-site',
site: this.server.getVideoTab(),
tabHostname: await this.getCurrentTabHostname()
});
}
if (message.cmd === 'popup-set-selected-tab') {
this.server.setSelectedTab(message.selectedMenu, message.selectedSubitem);
}
if (message.cmd === 'get-config') {
if(Debug.debug) {
console.log("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}
);
} else if (message.cmd === 'has-video') {
this.server.registerVideo(port.sender);
} else if (message.cmd === 'noVideo') {
this.server.unregisterVideo(port.sender);
}
}
processReceivedMessage_nonpersistent(message, sender, sendResponse){
if (Debug.debug && Debug.comms) {
console.log("%c[CommsServer.js::processMessage_nonpersistent] Received message from background script!", "background-color: #11D; color: #aad", message, sender);
}
if (message.forwardToContentScript) {
if (Debug.debug && Debug.comms) {
console.log("[CommsServer.js::processMessage_nonpersistent] Message has 'forward to content script' flag set. Forwarding message as is. Message:", message);
console.log("[CommsServer.js::processMessage_nonpersistent] (btw we probably shouldn't be seeing this. This should prolly happen in persistent connection?");
}
this.sendToFrame(message, message.targetFrame);
}
if (message.cmd === 'get-config') {
if (BrowserDetect.firefox) {
var ret = {extensionConf: JSON.stringify(this.settings.active)};
if (Debug.debug && Debug.comms) {
console.log("%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;
}
} else if (message.cmd === "autoar-enable") {
this.settings.active.sites['@global'].autoar = "blacklist";
this.settings.save();
this.sendToAll({cmd: "reload-settings", sender: "uwbg"})
if(Debug.debug){
console.log("[uw-bg] autoar set to enabled (blacklist). evidenz:", this.settings.active);
}
} else if (message.cmd === "autoar-disable") {
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.sendToAll({cmd: 'reload-settings', newConf: this.settings.active});
if(Debug.debug){
console.log("[uw-bg] autoar set to disabled. evidenz:", this.settings.active);
}
} else if (message.cmd === "autoar-set-interval") {
if (Debug.debug) {
console.log("[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();
this.sendToAll({cmd: 'reload-settings', newConf: this.settings.active});
}
}
}
export default CommsServer;

View File

@ -0,0 +1,488 @@
import Debug from '../../conf/Debug';
import VideoData from './VideoData';
import RescanReason from './enums/RescanReason';
import AspectRatio from '../../../common/enums/aspect-ratio.enum';
if(Debug.debug)
console.log("Loading: PageInfo.js");
class PageInfo {
constructor(comms, settings, extensionMode){
this.hasVideos = false;
this.siteDisabled = false;
this.videos = [];
this.settings = settings;
this.actionHandlerInitQueue = [];
this.lastUrl = window.location.href;
this.extensionMode = extensionMode;
if(comms){
this.comms = comms;
}
this.rescan(RescanReason.PERIODIC);
this.scheduleUrlCheck();
this.currentZoomScale = 1;
}
destroy() {
if(Debug.debug || Debug.init){
console.log("[PageInfo::destroy] destroying all videos!")
}
if(this.rescanTimer){
clearTimeout(this.rescanTimer);
}
for (var video of this.videos) {
this.comms.unregisterVideo(video.id)
video.destroy();
}
}
reset() {
for(var video of this.videos) {
video.destroy();
}
this.rescan(RescanReason.MANUAL);
}
initMouseActionHandler(videoData) {
if (this.actionHandler) {
this.actionHandler.registerHandleMouse(videoData);
} else {
this.actionHandlerInitQueue.push(videoData);
}
}
setActionHandler(actionHandler) {
this.actionHandler = actionHandler;
for (var item of this.actionHandlerInitQueue) {
this.actionHandler.registerHandleMouse(item);
}
this.actionHandlerInitQueue = [];
}
rescan(rescanReason){
const oldVideoCount = this.videos.length;
try{
var vids = document.getElementsByTagName('video');
if(!vids || vids.length == 0){
this.hasVideos = false;
if(rescanReason == RescanReason.PERIODIC){
this.scheduleRescan(RescanReason.PERIODIC);
}
return;
}
// add new videos
this.hasVideos = false;
var videoExists = false;
var video, v;
for (video of vids) {
// če najdemo samo en video z višino in širino, to pomeni, da imamo na strani veljavne videe
// če trenutni video nima definiranih teh vrednostih, preskočimo vse nadaljnja preverjanja
// <===[:::::::]===>
// if we find even a single video with width and height, that means the page has valid videos
// if video lacks either of the two properties, we skip all further checks cos pointless
if(video.offsetWidth && video.offsetHeight){
this.hasVideos = true;
} else {
continue;
}
videoExists = false;
for (v of this.videos) {
if (v.destroyed) {
continue; //TODO: if destroyed video is same as current video, copy aspect ratio settings to current video
}
if (v.video == video) {
videoExists = true;
break;
}
}
if (videoExists) {
continue;
} else {
if (Debug.debug && Debug.periodic && Debug.videoRescan) {
console.log("[PageInfo::rescan] found new video candidate:", video, "NOTE:: Video initialization starts here:\n--------------------------------\n")
}
v = new VideoData(video, this.settings, this);
// console.log("[PageInfo::rescan] v is:", v)
v.initArDetection();
this.videos.push(v);
if(Debug.debug && Debug.periodic && Debug.videoRescan){
console.log("[PageInfo::rescan] END VIDEO INITIALIZATION\n\n\n-------------------------------------\nvideos[] is now this:", this.videos,"\n\n\n\n\n\n\n\n")
}
}
}
this.removeDestroyed();
// če smo ostali brez videev, potem odregistriraj stran.
// če nismo ostali brez videev, potem registriraj stran.
//
// if we're left withotu videos on the current page, we unregister the page.
// if we have videos, we call register.
// if(Debug.debug) {
// console.log("[PageInfo::rescan] Comms:", this.comms, "\nvideos.length:", this.videos.length, "\nold video count:", oldVideoCount)
// }
if (this.comms) {
if (this.videos.length != oldVideoCount) { // only if number of videos changed, tho
if (this.videos.length > 0) {
this.comms.registerVideo({host: window.location.host, location: window.location});
} else {
this.comms.unregisterVideo({host: window.location.host, location: window.location});
}
}
}
} catch(e) {
// če pride do zajeba, potem lahko domnevamo da na strani ni nobenega videa. Uničimo vse objekte videoData
// da preprečimo večkratno inicializacijo. Če smo se z našim ugibom zmotili, potem se bodo vsi videi ponovno
// našli ob naslednjem preiskovanju
//
// if we encounter a fuckup, we can assume that no videos were found on the page. We destroy all videoData
// objects to prevent multiple initalization (which happened, but I don't know why). No biggie if we destroyed
// videoData objects in error — they'll be back in the next rescan
if (Debug.debug) {
console.log("rescan error: — destroying all videoData objects",e);
}
for (const v of this.videos) {
v.destroy();
}
return;
}
if(rescanReason == RescanReason.PERIODIC){
this.scheduleRescan(RescanReason.PERIODIC);
}
}
removeDestroyed(){
this.videos = this.videos.filter( vid => vid.destroyed === false);
}
scheduleRescan(rescanReason){
if(rescanReason != RescanReason.PERIODIC){
this.rescan(rescanReason);
return;
}
try{
if(this.rescanTimer){
clearTimeout(this.rescanTimer);
}
var ths = this;
this.rescanTimer = setTimeout(function(rr){
ths.rescanTimer = null;
ths.rescan(rr);
ths = null;
}, rescanReason === this.settings.active.pageInfo.timeouts.rescan, RescanReason.PERIODIC)
} catch(e) {
if(Debug.debug){
console.log("[PageInfo::scheduleRescan] scheduling rescan failed. Here's why:",e)
}
}
}
scheduleUrlCheck() {
try{
if(this.urlCheckTimer){
clearTimeout(this.urlCheckTimer);
}
var ths = this;
this.rescanTimer = setTimeout(function(){
ths.rescanTimer = null;
ths.ghettoUrlCheck();
ths = null;
}, this.settings.active.pageInfo.timeouts.urlCheck)
}catch(e){
if(Debug.debug){
console.log("[PageInfo::scheduleUrlCheck] scheduling URL check failed. Here's why:",e)
}
}
}
ghettoUrlCheck() {
if (this.lastUrl != window.location.href){
if(Debug.debug){
console.log("[PageInfo::ghettoUrlCheck] URL has changed. Triggering a rescan!");
}
this.rescan(RescanReason.URL_CHANGE);
this.lastUrl = window.location.href;
}
this.scheduleUrlCheck();
}
initArDetection(playingOnly){
if (playingOnly) {
for(var vd of this.videos){
if(vd.isPlaying()) {
vd.initArDetection();
}
}
return;
} else {
for(var vd of this.videos){
vd.initArDetection();
}
}
}
// to je treba klicat ob menjavi zavihkov
// these need to be called on tab switch
pauseProcessing(playingOnly){
if (playingOnly) {
for(var vd of this.videos){
if (vd.isPlaying()) {
vd.pause();
}
}
} else {
for(var vd of this.videos){
vd.pause();
}
}
}
resumeProcessing(resumeAutoar = false, playingOnly = false){
if (playingOnly) {
for(var vd of this.videos){
if (vd.isPlaying()) {
vd.resume();
if(resumeAutoar){
vd.resumeAutoAr();
}
}
}
} else {
for(var vd of this.videos){
vd.resume();
if(resumeAutoar){
vd.resumeAutoAr();
}
}
}
}
startArDetection(playingOnly){
if (Debug.debug) {
console.log('[PageInfo::startArDetection()] starting automatic ar detection!')
}
if (playingOnly) {
for(var vd of this.videos){
if (video.isPlaying()) {
vd.startArDetection();
}
}
} else {
for(var vd of this.videos){
vd.startArDetection();
}
}
}
stopArDetection(playingOnly){
if (playingOnly) {
for(var vd of this.videos){
if (vd.isPlaying()) {
vd.stopArDetection();
}
}
} else {
for(var vd of this.videos){
vd.stopArDetection();
}
}
}
setAr(ar, playingOnly){
if (Debug.debug) {
console.log('[PageInfo::setAr] aspect ratio:', ar, "playing only?", playingOnly)
}
if (ar.type !== AspectRatio.Automatic) {
this.stopArDetection(playingOnly);
} else {
if (Debug.debug) {
console.log('[PageInfo::setAr] aspect ratio is auto');
}
try {
for (var vd of this.videos) {
if (!playingOnly || vd.isPlaying()) {
vd.resetLastAr();
}
}
} catch (e) {
console.log("???", e);
}
this.initArDetection(playingOnly);
this.startArDetection(playingOnly);
return;
}
// TODO: find a way to only change aspect ratio for one video
if (ar === AspectRatio.Reset) {
for (var vd of this.videos) {
if (!playingOnly || vd.isPlaying()) {
vd.resetAr();
}
}
} else {
for (var vd of this.videos) {
if (!playingOnly || vd.isPlaying()) {
vd.setAr(ar)
}
}
}
}
setvideoAlignment(videoAlignment, playingOnly) {
if (playingOnly) {
for(var vd of this.videos) {
if (vd.isPlaying()) {
vd.setvideoAlignment(videoAlignment)
}
}
} else {
for(var vd of this.videos) {
vd.setvideoAlignment(videoAlignment)
}
}
}
setPanMode(mode, playingOnly) {
if (playingOnly) {
for(var vd of this.videos) {
if (vd.isPlaying()) {
vd.setPanMode(mode);
}
}
} else {
for(var vd of this.videos) {
vd.setPanMode(mode);
}
}
}
restoreAr(playingOnly) {
if (playingOnly) {
for(var vd of this.videos){
if (vd.isPlaying()) {
vd.restoreAr();
}
}
} else {
for(var vd of this.videos){
vd.restoreAr();
}
}
}
setStretchMode(sm, playingOnly){
// TODO: find a way to only change aspect ratio for one video
if (playingOnly) {
for(var vd of this.videos){
if (vd.isPlaying()) {
vd.setStretchMode(sm)
}
}
} else {
for(var vd of this.videos){
vd.setStretchMode(sm)
}
}
}
setZoom(zoomLevel, no_announce, playingOnly) {
if (playingOnly) {
for(var vd of this.videos) {
if (vd.isPlaying()) {
vd.setZoom(zoomLevel, no_announce);
}
}
} else {
for(var vd of this.videos) {
vd.setZoom(zoomLevel, no_announce);
}
}
}
zoomStep(step, playingOnly) {
for(var vd of this.videos){
if (!playingOnly || vd.isPlaying()) {
vd.zoomStep(step);
}
}
}
markPlayer(name, color) {
for (var vd of this.videos) {
vd.markPlayer(name,color);
}
}
unmarkPlayer() {
for (var vd of this.videos) {
vd.unmarkPlayer();
}
}
announceZoom(scale) {
if (this.announceZoomTimeout) {
clearTimeout(this.announceZoom);
}
this.currentZoomScale = scale;
const ths = this;
this.announceZoomTimeout = setTimeout(() => ths.comms.announceZoom(scale), this.settings.active.zoom.announceDebounce);
}
setManualTick(manualTick) {
for(var vd of this.videos) {
vd.setManualTick();
}
}
tick() {
for(var vd of this.videos) {
vd.tick();
}
}
sendPerformanceUpdate(performanceUpdate) {
if(this.comms) {
this.comms.sendPerformanceUpdate(performanceUpdate);
}
}
requestCurrentZoom() {
this.comms.announceZoom(this.currentZoomScale);
}
setKeyboardShortcutsEnabled(state) {
this.actionHandler.setKeybordLocal(state);
}
}
export default PageInfo;

View File

@ -1,3 +1,7 @@
import Debug from '../../conf/Debug';
import ExtensionMode from '../../../common/enums/extension-mode.enum'
import AspectRatio from '../../../common/enums/aspect-ratio.enum';
if(Debug.debug)
console.log("Loading: PlayerData.js");
@ -33,11 +37,14 @@ class PlayerData {
this.videoData = videoData;
this.video = videoData.video;
this.settings = videoData.settings;
this.extensionMode = videoData.extensionMode;
this.element = undefined;
this.dimensions = undefined;
this.overlayNode = undefined;
this.getPlayerDimensions();
if (this.extensionMode === ExtensionMode.Enabled) {
this.getPlayerDimensions();
}
this.startChangeDetection();
}
@ -45,9 +52,6 @@ class PlayerData {
return ( window.innerHeight == window.screen.height && window.innerWidth == window.screen.width);
}
panListener(event) {
this.panHandler(event);
}
start(){
this.startChangeDetection();
@ -59,8 +63,8 @@ class PlayerData {
}
destroy() {
this.element.removeEventListener('mousemove', this.panListener);
this.stopChangeDetection();
this.destroyOverlay();
}
startChangeDetection(){
@ -70,6 +74,58 @@ class PlayerData {
clearTimeout(this.watchTimeout);
}
makeOverlay() {
if (!this.overlayNode) {
this.destroyOverlay();
}
var overlay = document.createElement('div');
overlay.style.width = '100%';
overlay.style.height = '100%';
overlay.style.position = 'absolute';
overlay.style.top = '0';
overlay.style.left = '0';
overlay.style.zIndex = '1000000000';
overlay.style.pointerEvents = 'none';
this.overlayNode = overlay;
this.element.appendChild(overlay);
}
destroyOverlay() {
if(this.playerIdElement) {
this.playerIdElement.remove();
this.playerIdElement = undefined;
}
if (this.overlayNode) {
this.overlayNode.remove();
this.overlayNode = undefined;
}
}
markPlayer(name, color) {
if (!this.overlayNode) {
this.makeOverlay();
}
if (this.playerIdElement) {
this.playerIdElement.remove();
}
this.playerIdElement = document.createElement('div');
this.playerIdElement.innerHTML = `<div style="background-color: ${color}; color: #fff; position: absolute; top: 0; left: 0">${name}</div>`;
this.overlayNode.appendChild(this.playerIdElement);
}
unmarkPlayer() {
if (Debug.debug) {
console.log("[PlayerData::unmarkPlayer] unmarking player!")
}
if (this.playerIdElement) {
this.playerIdElement.remove();
}
this.playerIdElement = undefined;
}
scheduleGhettoWatcher(timeout, force_reset) {
if(! timeout){
timeout = 100;
@ -102,8 +158,7 @@ class PlayerData {
);
}
ghettoWatcher(){
ghettoWatcherFull() {
if(this.checkPlayerSizeChange()){
if(Debug.debug){
console.log("[uw::ghettoOnChange] change detected");
@ -111,13 +166,10 @@ class PlayerData {
this.getPlayerDimensions();
if(! this.element ){
this.scheduleGhettoWatcher();
return;
}
this.videoData.resizer.restore(); // note: this returns true if change goes through, false otherwise.
this.scheduleGhettoWatcher();
return;
}
@ -134,20 +186,45 @@ class PlayerData {
this.getPlayerDimensions();
if(! this.element ){
this.scheduleGhettoWatcher();
return;
}
this.videoData.resizer.restore();
}
this.scheduleGhettoWatcher();
}
panHandler(event) {
this.videoData.panHandler(event);
ghettoWatcherBasic() {
if (this.checkFullscreenChange()) {
if (PlayerData.isFullScreen()) {
const lastAr = this.videoData.resizer.getLastAr(); // save last ar for restore later
this.videoData.resizer.restore();
if (lastAr.type === 'original' || lastAr.type === AspectRatio.Automatic) {
this.videoData.rebootArDetection();
}
} else {
const lastAr = this.videoData.resizer.getLastAr(); // save last ar for restore later
this.videoData.resizer.reset();
this.videoData.resizer.stop();
this.videoData.stopArDetection();
this.videoData.resizer.setLastAr(lastAr);
}
}
}
ghettoWatcher(){
if (this.extensionMode === ExtensionMode.Enabled) {
this.ghettoWatcherFull();
this.scheduleGhettoWatcher();
} else if (this.extensionMode === ExtensionMode.Basic) {
this.ghettoWatcherBasic();
this.scheduleGhettoWatcher();
}
}
getPlayerDimensions(elementNames){
// element names — reserved for future use. If element names are provided, this function should return first element that
// has classname or id that matches at least one in the elementNames array.
@ -159,7 +236,6 @@ class PlayerData {
if(this.element) {
const ths = this;
this.element.removeEventListener('mousemove', this.panListener);
}
this.element = undefined;
this.dimensions = undefined;
@ -226,11 +302,11 @@ class PlayerData {
fullscreen: true
}
const ths = this;
if(this.element) {
this.element.removeEventListener('mousemove', (event) => ths.panListener);
if (this.element != element) {
this.element = element;
this.makeOverlay()
}
this.element = element;
this.element.addEventListener('mousemove', ths.panListener);
} else {
this.dimensions = {
width: candidate_width,
@ -238,11 +314,10 @@ class PlayerData {
fullscreen: isFullScreen
};
const ths = this;
if(this.element) {
this.element.removeEventListener('mousemove', (event) => ths.panListener);
if(this.element != playerCandidateNode) {
this.element = playerCandidateNode;
this.makeOverlay();
}
this.element = playerCandidateNode;
this.element.addEventListener('mousemove', (event) => ths.panListener);
}
}
@ -280,5 +355,34 @@ class PlayerData {
return false;
}
checkFullscreenChange() {
const isFs = PlayerData.isFullScreen();
if (this.dimensions) {
if (this.dimensions.fullscreen != isFs) {
this.dimensions = {
fullscreen: isFs,
width: isFs ? screen.width : this.video.offsetWidth,
height: isFs ? screen.height : this.video.offsetHeight
};
return true;
}
return false;
}
if(Debug.debug) {
console.log("[PlayerData::checkFullscreenChange] this.dimensions is not defined. Assuming fs change happened and setting default values.")
}
this.dimensions = {
fullscreen: isFs,
width: isFs ? screen.width : this.video.offsetWidth,
height: isFs ? screen.height : this.video.offsetHeight
};
return true;
}
}
export default PlayerData;

View File

@ -1,3 +1,8 @@
import Debug from '../../conf/Debug';
import PlayerData from './PlayerData';
import Resizer from '../video-transform/Resizer';
import ArDetector from '../ar-detect/ArDetector';
class VideoData {
constructor(video, settings, pageInfo){
@ -5,6 +10,10 @@ class VideoData {
this.video = video;
this.destroyed = false;
this.settings = settings;
this.pageInfo = pageInfo;
this.extensionMode = pageInfo.extensionMode;
// POZOR: VRSTNI RED JE POMEMBEN (arDetect mora bit zadnji)
// NOTE: ORDERING OF OBJ INITIALIZATIONS IS IMPORTANT (arDetect needs to go last)
this.player = new PlayerData(this);
@ -13,11 +22,20 @@ class VideoData {
this.arDetector = new ArDetector(this); // this starts Ar detection. needs optional parameter that prevets ardetdctor from starting
// player dimensions need to be in:
// this.player.dimensions
this.pageInfo = pageInfo;
// apply default align and stretch
if (Debug.init) {
console.log("%c[VideoData::ctor] Initial resizer reset!", {background: '#afd', color: '#132'});
}
this.resizer.reset();
this.vdid = (Math.random()*100).toFixed();
if (Debug.init) {
console.log("[VideoData::ctor] Created videoData with vdid", this.vdid);
console.log("[VideoData::ctor] Created videoData with vdid", this.vdid,"\nextension mode:", this.extensionMode);
}
this.pageInfo.initMouseActionHandler(this);
}
firstTimeArdInit(){
@ -43,6 +61,9 @@ class VideoData {
}
startArDetection() {
if (Debug.debug) {
console.log("[VideoData::startArDetection] starting AR detection")
}
if(this.destroyed) {
throw {error: 'VIDEO_DATA_DESTROYED', data: {videoData: this}};
}
@ -52,6 +73,13 @@ class VideoData {
this.arDetector.start();
}
rebootArDetection() {
if(this.destroyed) {
throw {error: 'VIDEO_DATA_DESTROYED', data: {videoData: this}};
}
this.arDetector.init();
}
stopArDetection() {
if (this.arDetector) {
this.arDetector.stop();
@ -101,7 +129,9 @@ class VideoData {
this.paused = false;
try {
this.resizer.start();
this.player.start();
if (this.player) {
this.player.start();
}
} catch (e) {
if(Debug.debug){
console.log("[VideoData.js::resume] cannot resume for reasons. Will destroy videoData. Error here:", e);
@ -116,6 +146,18 @@ class VideoData {
}
}
setManualTick(manualTick) {
if(this.arDetector){
this.arDetector.setManualTick(manualTick);
}
}
tick() {
if(this.arDetector){
this.arDetector.tick();
}
}
setLastAr(lastAr){
this.resizer.setLastAr(lastAr);
}
@ -128,7 +170,11 @@ class VideoData {
this.resizer.reset();
}
panHandler(event) {
resetLastAr() {
this.resizer.setLastAr('original');
}
panHandler(event, forcePan) {
if(this.destroyed) {
throw {error: 'VIDEO_DATA_DESTROYED', data: {videoData: this}};
}
@ -136,15 +182,15 @@ class VideoData {
this.destroy();
return;
}
this.resizer.panHandler(event);
this.resizer.panHandler(event, forcePan);
}
setPanMode(mode) {
this.resizer.setPanMode(mode);
}
setVideoFloat(videoFloat) {
this.resizer.setVideoFloat(videoFloat);
setvideoAlignment(videoAlignment) {
this.resizer.setvideoAlignment(videoAlignment);
}
restoreAr(){
@ -166,4 +212,24 @@ class VideoData {
announceZoom(scale){
this.pageInfo.announceZoom(scale);
}
}
markPlayer(name, color) {
if (this.player) {
this.player.markPlayer(name, color)
}
}
unmarkPlayer() {
this.player.unmarkPlayer();
}
isPlaying() {
console.log("is playing? video:", this.video, "ctime:", this.video.currentTime,
"paused/ended:", this.video.paused, this.video.ended,
"is playing?", this.video && this.video.currentTime > 0 && !this.video.paused && !this.video.ended);
return this.video && this.video.currentTime > 0 && !this.video.paused && !this.video.ended;
}
}
export default VideoData;

View File

@ -0,0 +1,7 @@
var RescanReason = Object.freeze({
PERIODIC: 0,
URL_CHANGE: 1,
MANUAL: 2
});
export default RescanReason;

View File

@ -0,0 +1,676 @@
import Debug from '../../conf/Debug';
import Scaler from './Scaler';
import Stretcher from './Stretcher';
import Zoom from './Zoom';
import PlayerData from '../video-data/PlayerData';
import ExtensionMode from '../../../common/enums/extension-mode.enum';
import Stretch from '../../../common/enums/stretch.enum';
import VideoAlignment from '../../../common/enums/video-alignment.enum';
import AspectRatio from '../../../common/enums/aspect-ratio.enum';
if(Debug.debug) {
console.log("Loading: Resizer.js");
}
class Resizer {
constructor(videoData) {
this.conf = videoData;
this.video = videoData.video;
this.settings = videoData.settings;
this.extensionMode = videoData.extensionMode;
this.scaler = new Scaler(this.conf);
this.stretcher = new Stretcher(this.conf);
this.zoom = new Zoom(this.conf);
this.cssCheckDisabled = false;
// load up default values
this.correctedVideoDimensions = {};
this.currentCss = {};
this.currentStyleString = "";
this.currentCssValidFor = {};
// restore watchdog. While true, applyCss() tries to re-apply new css until this value becomes false again
// value becomes false when width and height of <video> tag match with what we want to set. Only necessary when
// calling _res_restore() for some weird reason.
this.restore_wd = false;
// CSS watcher will trigger _very_ often for this many iterations
this.cssWatcherIncreasedFrequencyCounter = 0;
// this.lastAr = this.settings.getDefaultAr(); // this is the aspect ratio we start with
this.lastAr = {type: AspectRatio.Initial};
this.videoAlignment = this.settings.getDefaultVideoAlignment(window.location.hostname); // this is initial video alignment
this.destroyed = false;
this.resizerId = (Math.random(99)*100).toFixed(0);
if (this.settings.active.pan) {
console.log("can pan:", this.settings.active.miscSettings.mousePan.enabled, "(default:", this.settings.active.miscSettings.mousePan.enabled, ")")
this.canPan = this.settings.active.miscSettings.mousePan.enabled;
} else {
this.canPan = false;
}
}
start(){
if(!this.destroyed) {
this.startCssWatcher();
}
}
stop(){
this.stopCssWatcher();
}
destroy(){
if(Debug.debug || Debug.init){
console.log(`[Resizer::destroy] <rid:${this.resizerId}> received destroy command.`);
}
this.destroyed = true;
this.stopCssWatcher();
}
calculateRatioForLegacyOptions(ar){
// also present as modeToAr in Scaler.js
if (ar.type !== AspectRatio.FitWidth && ar.type !== AspectRatio.FitHeight && ar.ratio) {
return ar;
}
// Skrbi za "stare" možnosti, kot na primer "na širino zaslona", "na višino zaslona" in "ponastavi".
// Približevanje opuščeno.
// handles "legacy" options, such as 'fit to widht', 'fit to height' and AspectRatio.Reset. No zoom tho
var ratioOut;
if (!this.conf.video) {
if (Debug.debug) {
console.log("[Scaler.js::modeToAr] No video??",this.conf.video, "killing videoData");
}
this.conf.destroy();
return null;
}
if (! this.conf.player.dimensions) {
ratioOut = screen.width / screen.height;
} else {
if (Debug.debug && Debug.debugResizer) {
console.log(`[Resizer::calculateRatioForLegacyOptions] <rid:${this.resizerId}> Player dimensions:`, this.conf.player.dimensions.width ,'x', this.conf.player.dimensions.height,'aspect ratio:', this.conf.player.dimensions.width / this.conf.player.dimensions.height)
}
ratioOut = this.conf.player.dimensions.width / this.conf.player.dimensions.height;
}
// POMEMBNO: lastAr je potrebno nastaviti šele po tem, ko kličemo _res_setAr(). _res_setAr() predvideva,
// da želimo nastaviti statično (type: 'static') razmerje stranic — tudi, če funkcijo kličemo tu oz. v ArDetect.
//
// IMPORTANT NOTE: lastAr needs to be set after _res_setAr() is called, as _res_setAr() assumes we're
// setting a static aspect ratio (even if the function is called from here or ArDetect).
var fileAr = this.conf.video.videoWidth / this.conf.video.videoHeight;
if (ar.type === AspectRatio.FitWidth){
ar.ratio = ratioOut > fileAr ? ratioOut : fileAr;
}
else if(ar.type === AspectRatio.FitHeight){
ar.ratio = ratioOut < fileAr ? ratioOut : fileAr;
}
else if(ar.type === AspectRatio.Reset){
if(Debug.debug){
console.log("[Scaler.js::modeToAr] Using original aspect ratio -", fileAr);
}
ar.ratio = fileAr;
} else {
return null;
}
return ar;
}
setAr(ar, lastAr){
if (this.destroyed) {
return;
}
if(Debug.debug){
console.log('[Resizer::setAr] <rid:'+this.resizerId+'> trying to set ar. New ar:', ar)
}
if (ar == null) {
return;
}
if (ar.type === AspectRatio.Automatic ||
ar.type === AspectRatio.Reset && this.lastAr.type === AspectRatio.Initial) {
// some sites do things that interfere with our site (and aspect ratio setting in general)
// first, we check whether video contains anything we don't like
const siteSettings = this.settings.active.sites[window.location.host];
if (siteSettings && siteSettings.autoarPreventConditions) {
if (siteSettings.autoarPreventConditions.videoStyleString) {
const styleString = (this.video.getAttribute('style') || '').split(';');
if (siteSettings.autoarPreventConditions.videoStyleString.containsProperty) {
const bannedProperties = siteSettings.autoarPreventConditions.videoStyleString.containsProperty;
for (const prop in bannedProperties) {
for (const s of styleString) {
if (s.trim().startsWith(prop)) {
// check if css property has a list of allowed values:
if (bannedProperties[prop].allowedValues) {
const styleValue = s.split(':')[1].trim();
// check if property value is on the list of allowed values
// if it's not, we aren't allowed to start aard
if (bannedProperties[prop].allowedValues.indexOf(styleValue) === -1) {
if (Debug.debug) {
console.log("%c[Resizer::setAr] video style contains forbidden css property/value combo: ", "color: #900, background: #100", prop, " — we aren't allowed to start autoar.")
}
return;
}
} else {
// no allowed values, no problem. We have forbidden property
// and this means aard can't start.
if (Debug.debug) {
console.log("%c[Resizer::setAr] video style contains forbidden css property: ", "color: #900, background: #100", prop, " — we aren't allowed to start autoar.")
}
return;
}
}
}
}
}
}
}
}
if (lastAr) {
this.lastAr = this.calculateRatioForLegacyOptions(lastAr);
ar = this.calculateRatioForLegacyOptions(ar);
} else {
// NOTE: "fitw" "fith" and "reset" should ignore ar.ratio bit, but
// I'm not sure whether they do. Check that.
ar = this.calculateRatioForLegacyOptions(ar);
if (! ar) {
if (Debug.debug && Debug.debugResizer) {
console.log(`[Resizer::setAr] <${this.resizerId}> Something wrong with ar or the player. Doing nothing.`);
}
return;
}
this.lastAr = {type: ar.type, ratio: ar.ratio}
}
if (this.extensionMode === ExtensionMode.Basic && !PlayerData.isFullScreen() && ar.type !== AspectRatio.Reset) {
// don't actually apply or calculate css when using basic mode if not in fullscreen
// ... unless we're resetting the aspect ratio to original
return;
}
if (! this.video) {
// console.log("No video detected.")
this.videoData.destroy();
}
if (this.extensionMode === ExtensionMode.Enabled || PlayerData.isFullScreen()) {
this.startCssWatcher();
}
this.cssWatcherIncreasedFrequencyCounter = 20;
// // pause AR on basic stretch, unpause when using other mdoes
// fir sine reason unpause doesn't unpause. investigate that later
try {
if (this.stretcher.mode === Stretch.Basic) {
this.conf.arDetector.pause();
} else {
if (this.lastAr.type === AspectRatio.Automatic) {
this.conf.arDetector.unpause();
}
}
} catch (e) { // resizer starts before arDetector. this will do nothing but fail if arDetector isn't setup
}
// do stretch thingy
if (this.stretcher.mode === Stretch.NoStretch || this.stretcher.mode === Stretch.Conditional){
var stretchFactors = this.scaler.calculateCrop(ar);
if(! stretchFactors || stretchFactors.error){
if(Debug.debug){
console.log("[Resizer::setAr] <rid:"+this.resizerId+"> failed to set AR due to problem with calculating crop. Error:", (stretchFactors ? stretchFactors.error : stretchFactors));
}
if (stretchFactors.error === 'no_video'){
this.conf.destroy();
}
if (stretchFactors.error === 'illegal_video_dimensions') {
if(Debug.debug){
console.log("[Resizer::setAr] <rid:"+this.resizerId+"> Illegal video dimensions found. We will pause everything.");
}
// if we get illegal video dimensions, cssWatcher goes nuts. This is harmful,
// so we stop it until that sorts itself out
this.stopCssWatcher();
}
return;
} else {
this.startCssWatcher();
}
if(this.stretcher.mode === Stretch.Conditional){
this.stretcher.applyConditionalStretch(stretchFactors, ar.ratio);
}
if (Debug.debug) {
console.log("[Resizer::setAr] Processed stretch factors for ", this.stretcher.mode === Stretch.NoStretch ? 'stretch-free crop.' : 'crop with conditional stretch.', 'Stretch factors are:', stretchFactors);
}
} else if (this.stretcher.mode === Stretch.Hybrid) {
var stretchFactors = this.stretcher.calculateStretch(ar.ratio);
if (Debug.debug) {
console.log('[Resizer::setAr] Processed stretch factors for hybrid stretch/crop. Stretch factors are:', stretchFactors);
}
} else if (this.stretcher.mode === Stretch.Basic) {
var stretchFactors = this.stretcher.calculateBasicStretch();
if (Debug.debug) {
console.log('[Resizer::setAr] Processed stretch factors for basic stretch. Stretch factors are:', stretchFactors);
}
} else {
var stretchFactors = {xFactor: 1, yFactor: 1}
if (Debug.debug) {
console.log('[Resizer::setAr] Okay wtf happened? If you see this, something has gone wrong', stretchFactors,"\n------[ i n f o d u m p ]------\nstretcher:", this.stretcher);
}
}
this.zoom.applyZoom(stretchFactors);
//TODO: correct these two
var translate = this.computeOffsets(stretchFactors);
this.applyCss(stretchFactors, translate);
}
resetLastAr() {
this.lastAr = {type: AspectRatio.Initial};
}
setLastAr(override){
this.lastAr = override;
}
getLastAr(){
return this.lastAr;
}
setStretchMode(stretchMode){
this.stretcher.setStretchMode(stretchMode);
this.restore();
}
panHandler(event, forcePan) {
// console.log("this.conf.canPan:", this.conf.canPan)
if (this.canPan || forcePan) {
if(!this.conf.player || !this.conf.player.element) {
return;
}
// dont allow weird floats
this.videoAlignment = VideoAlignment.Center;
const player = this.conf.player.element;
const relativeX = (event.pageX - player.offsetLeft) / player.offsetWidth;
const relativeY = (event.pageY - player.offsetTop) / player.offsetHeight;
if (Debug.debug && Debug.mousemove) {
console.log("[Resizer::panHandler] mousemove.pageX, pageY:", event.pageX, event.pageY,
"\nrelativeX/Y:", relativeX, relativeY)
}
this.setPan(relativeX, relativeY);
}
}
setPan(relativeMousePosX, relativeMousePosY){
// relativeMousePos[X|Y] - on scale from 0 to 1, how close is the mouse to player edges.
// use these values: top, left: 0, bottom, right: 1
if(! this.pan){
this.pan = {};
}
if (this.settings.active.miscSettings.mousePanReverseMouse) {
this.pan.relativeOffsetX = (relativeMousePosX * 1.1) - 0.55;
this.pan.relativeOffsetY = (relativeMousePosY * 1.1) - 0.55;
} else {
this.pan.relativeOffsetX = -(relativeMousePosX * 1.1) + 0.55;
this.pan.relativeOffsetY = -(relativeMousePosY * 1.1) + 0.55;
}
// if(Debug.debug){
// console.log("[Resizer::setPan] relative cursor pos:", relativeMousePosX, ",",relativeMousePosY, " | new pan obj:", this.pan)
// }
this.restore();
}
setvideoAlignment(videoAlignment) {
this.videoAlignment = videoAlignment;
this.restore();
}
startCssWatcher(){
if(Debug.debug) {
console.log("[Resizer.js::startCssWatcher] starting css watcher. Is resizer destroyed?", this.destroyed);
}
if (this.destroyed) {
return;
}
this.cssCheckDisabled = false;
if(!this.cssWatcherTimer){
this.scheduleCssWatcher(1);
} else {
clearTimeout(this.cssWatcherTimer);
this.scheduleCssWatcher(1);
}
}
scheduleCssWatcher(timeout, force_reset) {
if (this.destroyed || (this.extensionMode !== ExtensionMode.Enabled && !PlayerData.isFullScreen())) {
return;
}
if(timeout === undefined) {
this.cssCheck(); // no timeout = one-off
return;
}
// one extra check to ensure we don't run css checks when we aren't supposed to
if (this.cssCheckDisabled) {
return;
}
if(this.cssWatcherTimeout) {
clearTimeout(this.cssWatcherTimer);
}
var ths = this;
this.cssWatcherTimer = setTimeout(function () {
ths.cssWatcherTimer = null;
try {
if (! ths.cssCheckDisabled) {
ths.cssCheck();
}
} catch (e) {
if(Debug.debug) {
console.log("[Resizer.js::scheduleCssWatcher] Css check failed. Error:", e);
}
}
}, timeout);
}
stopCssWatcher() {
if (Debug.debug) {
console.log(`[Resizer.js] <${this.resizerId}> STOPPING CSS WATCHER!`)
}
this.cssCheckDisabled = true;
clearInterval(this.cssWatcherTimeout);
}
restore() {
if(Debug.debug){
console.log("[Resizer::restore] <rid:"+this.resizerId+"> attempting to restore aspect ratio. this & settings:", {'a_lastAr': this.lastAr, 'this': this, "settings": this.settings} );
}
// this is true until we verify that css has actually been applied
this.restore_wd = true;
if(this.lastAr.type === AspectRatio.Initial){
this.setAr({type: AspectRatio.Reset});
}
else {
if (this.lastAr && this.lastAr.ratio === null) {
console.log("[Resizer::restore] LAST AR IS NULL")
throw "Last ar is null!"
}
this.setAr(this.lastAr, this.lastAr)
}
}
reset(){
this.setStretchMode(this.settings.active.sites[window.location.hostname] ? this.settings.active.sites[window.location.hostname].stretch : this.settings.active.sites['@global'].stretch);
this.zoom.setZoom(1);
this.resetPan();
this.setAr({type: AspectRatio.Reset});
}
setPanMode(mode) {
if (mode === 'enable') {
this.canPan = true;
} else if (mode === 'disable') {
this.canPan = false;
} else if (mode === 'toggle') {
this.canPan = !this.canPan;
}
}
resetPan(){
this.pan = undefined;
}
setZoom(zoomLevel, no_announce) {
this.zoom.setZoom(zoomLevel, no_announce);
}
zoomStep(step){
this.zoom.zoomStep(step);
}
resetZoom(){
this.zoom.setZoom(1);
this.restore();
}
resetCrop(){
this.setAr({type: AspectRatio.Reset});
}
resetStretch(){
this.stretcher.setStretchMode(Stretch.NoStretch);
this.restore();
}
// mostly internal stuff
computeOffsets(stretchFactors){
if (Debug.debug) {
console.log("[Resizer::computeOffsets] <rid:"+this.resizerId+"> video will be aligned to ", this.settings.active.sites['@global'].videoAlignment);
}
const wdiff = this.conf.player.dimensions.width - this.conf.video.offsetWidth;
const hdiff = this.conf.player.dimensions.height - this.conf.video.offsetHeight;
const wdiffAfterZoom = this.conf.video.offsetWidth * stretchFactors.xFactor - this.conf.player.dimensions.width;
const hdiffAfterZoom = this.conf.video.offsetHeight * stretchFactors.yFactor - this.conf.player.dimensions.height;
var translate = {
x: wdiff * 0.5,
y: hdiff * 0.5,
};
if (this.pan) {
// don't offset when video is smaller than player
if(wdiffAfterZoom < 0 && hdiffAfterZoom < 0) {
return translate;
}
translate.x += wdiffAfterZoom * this.pan.relativeOffsetX * this.zoom.scale;
translate.y += hdiffAfterZoom * this.pan.relativeOffsetY * this.zoom.scale;
} else {
if (this.videoAlignment == VideoAlignment.Left) {
translate.x += wdiffAfterZoom * 0.5;
}
else if (this.videoAlignment == VideoAlignment.Right) {
translate.x -= wdiffAfterZoom * 0.5;
}
}
if(Debug.debug) {
console.log("[Resizer::_res_computeOffsets] <rid:"+this.resizerId+"> calculated offsets:\n\n",
'---- data in ----\n',
'player dimensions:', {w: this.conf.player.dimensions.width, h: this.conf.player.dimensions.height},
'video dimensions: ', {w: this.conf.video.offsetWidth, h: this.conf.video.offsetHeight},
'stretch factors: ', stretchFactors,
'pan & zoom: ', this.pan, this.zoom,
'\n\n---- data out ----\n',
'translate:', translate);
}
return translate;
}
applyCss(stretchFactors, translate){
if (! this.video) {
if(Debug.debug) {
console.log("[Resizer::applyCss] <rid:"+this.resizerId+"> Video went missing, doing nothing.");
}
this.conf.destroy();
return;
}
// save stuff for quick tests (before we turn numbers into css values):
this.currentVideoSettings = {
validFor: this.conf.player.dimensions,
// videoWidth: dimensions.width,
// videoHeight: dimensions.height
}
var styleArrayStr = this.video.getAttribute('style');
var styleArrayObj = window.getComputedStyle(this.video);
if (styleArrayStr) {
var styleArray = styleArrayStr.split(";");
for(var i in styleArray){
styleArray[i] = styleArray[i].trim();
// some sites do 'top: 50%; left: 50%; transform: <transform>' to center videos.
// we dont wanna, because we already center videos on our own
if (styleArray[i].startsWith("transform:") ||
styleArray[i].startsWith("top:") ||
styleArray[i].startsWith("left:") ||
styleArray[i].startsWith("right:") ||
styleArray[i].startsWith("bottom:") ){
delete styleArray[i];
}
}
}else {
var styleArray = [];
}
// add remaining elements
if(stretchFactors){
styleArray.push(`transform: translate(${translate.x}px, ${translate.y}px) scale(${stretchFactors.xFactor}, ${stretchFactors.yFactor})`);
styleArray.push("top: 0px; left: 0px; bottom: 0px; right: 0px");
}
// build style string back
var styleString = "";
for(var i in styleArray)
if(styleArray[i])
styleString += styleArray[i] + "; ";
this.setStyleString(styleString);
}
setStyleString (styleString, count = 0) {
this.video.setAttribute("style", styleString);
this.currentStyleString = this.video.getAttribute('style');
this.currentCssValidFor = this.conf.player.dimensions;
if(this.restore_wd){
if(! this.video){
if(Debug.debug)
console.log("[Resizer::_res_setStyleString] <rid:"+this.resizerId+"> Video element went missing, nothing to do here.")
return;
}
// if(
// styleString.indexOf("width: " + this.video.style.width) == -1 ||
// styleString.indexOf("height: " + this.video.style.height) == -1) {
// // css ni nastavljen?
// // css not set?
// if(Debug.debug)
// console.log("[Resizer::_res_setStyleString] Style string not set ???");
// if(count < settings.active.resizer.setStyleString.maxRetries){
// setTimeout( this.setStyleString, settings.active.resizer.setStyleString.retryTimeout, count + 1);
// }
// else if(Debug.debug){
// console.log("[Resizer::_res_setStyleString] we give up. css string won't be set");
// }
// }
// else{
this.restore_wd = false;
// }
}
else{
if(Debug.debug)
console.log("[Resizer::_res_setStyleString] <rid:"+this.resizerId+"> css applied. Style string:", styleString);
}
}
cssCheck(){
if (this.cssCheckDisabled) {
throw "fucking dont"
return;
}
// this means we haven't set our CSS yet, or that we changed video.
// if(! this.currentCss.tranform) {
// this.scheduleCssWatcher(200);
// return;
// }
// this means video went missing. videoData will be re-initialized when the next video is found
if(! this.video){
if(Debug.debug && Debug.debugResizer) {
console.log("[Resizer::cssCheck] <rid:"+this.resizerId+"> no video detecting, issuing destroy command");
}
this.conf.destroy();
return;
}
if(this.destroyed) {
if(Debug.debug && Debug.debugResizer) {
console.log("[Resizer::cssCheck] <rid:"+this.resizerId+"> destroyed flag is set, we shouldnt be running");
}
this.stopCssWatcher();
return;
}
var styleString = this.video.getAttribute('style');
// first, a quick test:
// if (this.currentVideoSettings.validFor == this.conf.player.dimensions ){
if (this.currentStyleString !== styleString){
if(Debug.debug && Debug.debugResizer) {
console.log(`%c[Resizer::cssCheck] <rid:${this.resizerId}> something touched our style string. We need to re-apply the style.`, {background: '#ddf', color: '#007'});
}
this.restore();
this.scheduleCssWatcher(10);
}
if (this.cssWatcherIncreasedFrequencyCounter > 0) {
--this.cssWatcherIncreasedFrequencyCounter;
this.scheduleCssWatcher(20);
} else {
this.scheduleCssWatcher(1000);
}
}
}
export default Resizer;

View File

@ -1,3 +1,6 @@
import Debug from '../../conf/Debug';
import AspectRatio from '../../../common/enums/aspect-ratio.enum';
// računa velikost videa za približevanje/oddaljevanje
// does video size calculations for zooming/cropping
@ -9,12 +12,17 @@ class Scaler {
constructor(videoData) {
this.conf = videoData;
}
modeToAr(mode){
// Skrbi za "stare" možnosti, kot na primer "na širino zaslona", "na višino zaslona" in "ponastavi".
// Približevanje opuščeno.
// handles "legacy" options, such as 'fit to widht', 'fit to height' and 'reset'. No zoom tho
var ar;
// Skrbi za "stare" možnosti, kot na primer "na širino zaslona", "na višino zaslona" in "ponastavi".
// Približevanje opuščeno.
// handles "legacy" options, such as 'fit to widht', 'fit to height' and AspectRatio.Reset. No zoom tho
modeToAr (ar) {
if (ar.type !== AspectRatio.FitWidth && ar.type !== AspectRatio.FitHeight && ar.ratio) {
return ar.ratio;
}
var ratioOut;
if (!this.conf.video) {
if(Debug.debug){
@ -26,10 +34,10 @@ class Scaler {
if(! this.conf.player.dimensions ){
ar = screen.width / screen.height;
ratioOut = screen.width / screen.height;
}
else {
ar = this.conf.player.dimensions.width / this.conf.player.dimensions.height;
ratioOut = this.conf.player.dimensions.width / this.conf.player.dimensions.height;
}
// POMEMBNO: lastAr je potrebno nastaviti šele po tem, ko kličemo _res_setAr(). _res_setAr() predvideva,
@ -40,81 +48,85 @@ class Scaler {
var fileAr = this.conf.video.videoWidth / this.conf.video.videoHeight;
if (mode == "fitw"){
return ar > fileAr ? ar : fileAr;
if (ar.type === AspectRatio.FitWidth){
ratioOut > fileAr ? ratioOut : fileAr
ar.ratio = ratioOut;
return ratioOut;
}
else if(mode == "fith"){
return ar < fileAr ? ar : fileAr;
else if(ar.type === AspectRatio.FitHeight){
ratioOut < fileAr ? ratioOut : fileAr
ar.ratio = ratioOut;
return ratioOut;
}
else if(mode == "reset"){
else if(ar.type === AspectRatio.Reset){
if(Debug.debug){
console.log("[Scaler.js::modeToAr] Using original aspect ratio -", fileAr)
}
ar.ar = fileAr;
return fileAr;
}
return null;
}
calculateCrop(mode) {
if(!this.conf.video || this.conf.video.videoWidth == 0 || this.conf.video.videoHeight == 0){
if(Debug.debug)
console.log("[Scaler::calculateCrop] ERROR — no video detected.");
calculateCrop(ar) {
if(!this.conf.video){
if (Debug.debug) {
console.log("[Scaler::calculateCrop] ERROR — no video detected. Conf:", this.conf, "video:", this.conf.video, "video dimensions:", this.conf.video && this.conf.video.videoWidth, '×', this.conf.video && this.conf.video.videoHeight);
}
this.conf.destroy();
return {error: "no_video"};
}
if (this.conf.video.videoWidth == 0 || this.conf.video.videoHeight == 0) {
// that's illegal, but not illegal enough to just blast our shit to high hell
// mr officer will let you go with a warning this time around
if (Debug.debug) {
console.log("[Scaler::calculateCrop] Video has illegal dimensions. Video dimensions:", this.conf.video && this.conf.video.videoWidth, '×', this.conf.video && this.conf.video.videoHeight);
}
return {error: "illegal_video_dimensions"};
}
// če je 'ar' string, potem bomo z njim opravili v legacy wrapperju. Seveda obstaja izjema
// if 'ar' is string, we'll handle that in legacy wrapper, with one exception
if(mode === 'reset'){
if (ar.type === AspectRatio.Reset){
return {xFactor: 1, yFactor: 1}
}
var ar = 0;
if(isNaN(mode)){
ar = this.modeToAr(mode);
} else {
ar = mode;
}
// handle fuckie-wuckies
if (! ar){
if(Debug.debug)
console.log("[Scaler::calculateCrop] no ar?", ar, " -- we were given this mode:", mode);
return {error: "no_ar", ar: ar};
if (!ar.ratio){
if (Debug.debug && Debug.scaler) {
console.log("[Scaler::calculateCrop] no ar?", ar.ratio, " -- we were given this mode:", ar);
}
return {error: "no_ar", ratio: ar.ratio};
}
if(Debug.debug)
console.log("[Scaler::calculateCrop] trying to set ar. args are: ar->",ar,"; this.conf.player.dimensions->",this.conf.player.dimensions.width, "×", this.conf.player.dimensions.height, "| obj:", this.conf.player.dimensions);
if (Debug.debug && Debug.scaler) {
console.log("[Scaler::calculateCrop] trying to set ar. args are: ar->",ar.ratio,"; this.conf.player.dimensions->",this.conf.player.dimensions.width, "×", this.conf.player.dimensions.height, "| obj:", this.conf.player.dimensions);
}
if( (! this.conf.player.dimensions) || this.conf.player.dimensions.width === 0 || this.conf.player.dimensions.height === 0 ){
if(Debug.debug)
if (Debug.debug && Debug.scaler) {
console.log("[Scaler::calculateCrop] ERROR — no (or invalid) this.conf.player.dimensions:",this.conf.player.dimensions);
}
return {error: "this.conf.player.dimensions_error"};
}
// zdaj lahko končno začnemo računati novo velikost videa
// we can finally start computing required video dimensions now:
// Dejansko razmerje stranic datoteke/<video> značke
// Actual aspect ratio of the file/<video> tag
var fileAr = this.conf.video.videoWidth / this.conf.video.videoHeight;
var playerAr = this.conf.player.dimensions.width / this.conf.player.dimensions.height;
if(mode == "default" || !ar)
ar = fileAr;
if (ar.type === AspectRatio.Initial || !ar.ratio) {
ar.ratio = fileAr;
}
if(Debug.debug)
console.log("[Scaler::calculateCrop] ar is " ,ar, ", file ar is", fileAr, ", this.conf.player.dimensions are ", this.conf.player.dimensions.width, "×", this.conf.player.dimensions.height, "| obj:", this.conf.player.dimensions);
if (Debug.debug && Debug.scaler) {
console.log("[Scaler::calculateCrop] ar is " ,ar.ratio, ", file ar is", fileAr, ", this.conf.player.dimensions are ", this.conf.player.dimensions.width, "×", this.conf.player.dimensions.height, "| obj:", this.conf.player.dimensions);
}
var videoDimensions = {
xFactor: 1,
@ -127,22 +139,25 @@ class Scaler {
// console.log("[Scaler::calculateCrop] Player dimensions?", this.conf.player.dimensions.width, "×", this.conf.player.dimensions.height, "| obj:", this.conf.player.dimensions);
// }
if( fileAr < ar ){
if (fileAr < ar.ratio){
// imamo letterbox zgoraj in spodaj -> spremenimo velikost videa (a nikoli širše od ekrana)
// letterbox -> change video size (but never to wider than monitor width)
videoDimensions.xFactor = Math.min(ar, playerAr) / fileAr;
// if (Debug.debug && Debug.scaler) {
// console.log(`%c[Scaler::calculateCrop] Trying to determine scaling factors. Aspect ratios:\n file: ${fileAr.toFixed(3)}\n player: ${playerAr.toFixed(3)}\n target: ${ar.ratio.toFixed(3)}\n-----------------------`, "color: #2ba");
// }
videoDimensions.xFactor = Math.min(ar.ratio, playerAr) / fileAr;
videoDimensions.yFactor = videoDimensions.xFactor;
}
else {
videoDimensions.xFactor = fileAr / Math.min(ar, playerAr);
} else {
videoDimensions.xFactor = fileAr / Math.min(ar.ratio, playerAr);
videoDimensions.yFactor = videoDimensions.xFactor;
}
if(Debug.debug){
if (Debug.debug && Debug.scaler) {
console.log("[Scaler::calculateCrop] Crop factor calculated — ", videoDimensions.xFactor);
}
return videoDimensions;
}
}
}
export default Scaler;

View File

@ -1,9 +1,11 @@
import Debug from '../../conf/Debug';
import Stretch from '../../../common/enums/stretch.enum';
// računa vrednosti za transform-scale (x, y)
// transform: scale(x,y) se uporablja za raztegovanje videa, ne pa za približevanje
// calculates values for transform scale(x, y)
// transform: scale(x,y) is used for stretching, not zooming.
class Stretcher {
// internal variables
@ -12,7 +14,15 @@ class Stretcher {
constructor(videoData) {
this.conf = videoData;
this.settings = videoData.settings;
this.mode = this.settings.getDefaultStretchMode();
this.mode = this.settings.getDefaultStretchMode(window.location.hostname);
}
setStretchMode(stretchMode) {
if (stretchMode === Stretch.Default) {
this.mode = this.settings.getDefaultStretchMode(window.location.hostname);
} else {
this.mode = stretchMode;
}
}
applyConditionalStretch(stretchFactors, actualAr){
@ -177,4 +187,6 @@ class Stretcher {
return stretchFactors;
}
}
}
export default Stretcher;

View File

@ -1,10 +1,9 @@
// računa približevanje ter računa/popravlja odmike videa
import Debug from '../../conf/Debug';
// računa približevanje ter računa/popravlja odmike videa
// calculates zooming and video offsets/panning
class Zoom {
// internal variables
// functions
constructor(videoData) {
this.scale = 1;
@ -40,10 +39,14 @@ class Zoom {
}
setZoom(scale, no_announce){
if (Debug.debug) {
console.log("[Zoom::setZoom] Setting zoom to", scale, "!");
}
// NOTE: SCALE IS NOT LOGARITHMIC
if(scale < Math.pow(this.minScale)) {
if(scale < Math.pow(2, this.minScale)) {
scale = this.minScale;
} else if (scale > Math.pow(this.maxScale)) {
} else if (scale > Math.pow(2, this.maxScale)) {
scale = this.maxScale;
}
@ -55,8 +58,21 @@ class Zoom {
}
}
applyZoom(videoDimensions){
videoDimensions.xFactor *= this.scale;
videoDimensions.yFactor *= this.scale;
applyZoom(stretchFactors){
if (!stretchFactors) {
return;
}
if (Debug.debug) {
console.log("[Zoom::setZoom] Applying zoom. Stretch factors pre:", stretchFactors, " —> scale:", this.scale);
}
stretchFactors.xFactor *= this.scale;
stretchFactors.yFactor *= this.scale;
if (Debug.debug) {
console.log("[Zoom::setZoom] Applying zoom. Stretch factors post:", stretchFactors);
}
}
}
}
export default Zoom;

240
src/ext/uw-bg.js Normal file
View File

@ -0,0 +1,240 @@
import Debug from './conf/Debug.js';
import BrowserDetect from './conf/BrowserDetect';
import CommsServer from './lib/comms/CommsServer';
import Settings from './lib/Settings';
var BgVars = {
arIsActive: true,
hasVideos: false,
currentSite: ""
}
class UWServer {
constructor() {
this.ports = [];
this.arIsActive = true;
this.hasVideos = false;
this.currentSite = "";
this.setup();
this.videoTabs = {};
this.currentTabId = 0;
this._gctimeout = undefined;
this.selectedSubitem = {
'siteSettings': undefined,
'videoSettings': undefined,
}
}
async setup() {
this.settings = new Settings();
await this.settings.init();
this.comms = new CommsServer(this);
var ths = this;
if(BrowserDetect.firefox) {
browser.tabs.onActivated.addListener(function(m) {ths.onTabSwitched(m)});
} else if (BrowserDetect.chrome) {
chrome.tabs.onActivated.addListener(function(m) {ths.onTabSwitched(m)});
}
console.log("will schedule gcframe")
this.scheduleGc();
}
async _promisifyTabsGet(browserObj, tabId){
return new Promise( (resolve, reject) => {
browserObj.tabs.get(tabId, (tab) => resolve(tab));
});
}
scheduleGc(timeout) {
if (this._gctimeout) {
return;
}
if (!timeout) {
timeout = 0;
}
const ths = this;
setTimeout( () => {
clearTimeout(ths._gctimeout);
ths.gcFrames();
ths._gctimeoutgcTimeout = ths.scheduleGc(5000);
}, timeout);
}
extractHostname(url){
var hostname;
// 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;
if(Debug.debug)
console.log("[uw-bg::onTabSwitched] TAB CHANGED, GETTING INFO FROM MAIN TAB");
try {
this.currentTabId = activeInfo.tabId; // just for readability
var tab;
if (BrowserDetect.firefox) {
var tab = await browser.tabs.get(this.currentTabId);
} else if (BrowserDetect.chrome) {
var tab = await this._promisifyTabsGet(chrome, this.currentTabId);
}
this.currentSite = this.extractHostname(tab.url);
} catch(e) {
console.log(e);
}
if(Debug.debug) {
console.log("TAB SWITCHED!", this.currentSite)
}
this.selectedSubitem = {
'siteSettings': undefined,
'videoSettings': undefined,
}
//TODO: change extension icon based on whether there's any videos on current page
}
async gcFrames() {
// does "garbage collection" on frames
let frames;
if (BrowserDetect.firefox) {
frames = await browser.webNavigation.getAllFrames({tabId: this.currentTabId});
} else if (BrowserDetect.chrome) {
frames = await new Promise( (resolve, reject) => {
chrome.webNavigation.getAllFrames({tabId: this.currentTabId}, (data) => {resolve(data); return true});
});
}
if (this.videoTabs[this.currentTabId]) {
for (let key in this.videoTabs[this.currentTabId].frames) {
if (! frames.find(x => x.frameId == key)) {
delete this.videoTabs[this.currentTabId].frames[key];
}
}
}
}
registerVideo(sender) {
if (Debug.debug && Debug.comms) {
console.log("[UWServer::registerVideo] registering video.\nsender:", sender);
}
const tabHostname = this.extractHostname(sender.tab.url);
const frameHostname = this.extractHostname(sender.url);
// preveri za osirotele/zastarele vrednosti ter jih po potrebi izbriši
// check for orphaned/outdated values and remove them if neccessary
if (this.videoTabs[sender.tab.id]) {
if (this.videoTabs[sender.tab.id].host != tabHostname) {
delete this.videoTabs[sender.tab.id]
} else {
if(this.videoTabs[sender.tab.id].frames[sender.frameId]) {
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]) {
if (this.videoTabs[sender.tab.id].frames[sender.frameId]) {
return; // existing value is fine, no need to act
} else {
this.videoTabs[sender.tab.id].frames[sender.frameId] = {
id: sender.frameId,
host: frameHostname,
url: sender.url
}
}
} 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
}
}
if (Debug.debug && Debug.comms) {
console.log("[UWServer::registerVideo] video registered. current videoTabs:", this.videoTabs);
}
}
unregisterVideo(sender) {
if (Debug.debug && Debug.comms) {
console.log("[UWServer::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];
}
}
}
if (Debug.debug && Debug.comms) {
console.log("[UWServer::ungisterVideo] video unregistered. current videoTabs:", this.videoTabs);
}
}
setSelectedTab(menu, subitem) {
if (Debug.debug && Debug.comms) {
console.log("[uw-bg::setSelectedTab] saving selected tab for", menu, ":", subitem)
}
this.selectedSubitem[menu] = subitem;
}
getVideoTab() {
// friendly reminder: if current tab doesn't have a video,
// there won't be anything in this.videoTabs[this.currentTabId]
if (this.videoTabs[this.currentTabId]) {
return {
...this.videoTabs[this.currentTabId],
selected: this.selectedSubitem
};
}
// return something more or less empty if this tab doesn't have
// a video registered for it
return {
host: this.currentSite,
frames: [],
selected: this.selectedSubitem
}
}
}
var server = new UWServer();

View File

@ -1,3 +1,11 @@
import Debug from './conf/Debug';
import BrowserDetect from './conf/BrowserDetect';
import ExtensionMode from '../common/enums/extension-mode.enum'
import Settings from './lib/Settings';
import ActionHandler from './lib/ActionHandler';
import CommsClient from './lib/comms/CommsClient';
import PageInfo from './lib/video-data/PageInfo';
if(Debug.debug){
console.log("\n\n\n\n\n\n ——— Sᴛλʀᴛɪɴɢ Uʟᴛʀᴀɪɪʏ ———\n << ʟᴏᴀᴅɪɴɢ ᴍᴀɪɴ ꜰɪʟᴇ >>\n\n\n\n");
try {
@ -12,13 +20,16 @@ if(Debug.debug){
}
}
if (BrowserDetect.edge) {
HTMLCollection.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator];
}
class UW {
constructor(){
this.pageInfo = undefined;
this.comms = undefined;
this.settings = undefined;
this.keybinds = undefined;
this.actionHandler = undefined;
}
async init(){
@ -47,7 +58,14 @@ class UW {
// če smo razširitev onemogočili v nastavitvah, ne naredimo ničesar
// If extension is soft-disabled, don't do shit
if(! this.settings.canStartExtension()){
var extensionMode = this.settings.getExtensionMode();
if(Debug.debug) {
console.log("[uw::init] Extension mode:" + (extensionMode < 0 ? "disabled" : extensionMode == '1' ? 'basic' : 'full'));
}
if(extensionMode === ExtensionMode.Disabled){
if(Debug.debug) {
console.log("[uw::init] EXTENSION DISABLED, THEREFORE WONT BE STARTED")
}
@ -55,15 +73,23 @@ class UW {
}
try {
this.pageInfo = new PageInfo(this.comms, this.settings);
this.pageInfo = new PageInfo(this.comms, this.settings, extensionMode);
if(Debug.debug){
console.log("[uw.js::setup] pageInfo initialized. Here's the object:", this.pageInfo);
}
this.comms.setPageInfo(this.pageInfo);
this.keybinds = new Keybinds(this.pageInfo);
this.keybinds.setup();
if(Debug.debug) {
console.log("[uw.js::setup] will try to initate ActionHandler. Settings are:", this.settings, this.settings.active)
}
this.actionHandler = new ActionHandler(this.pageInfo);
this.actionHandler.init();
if(Debug.debug) {
console.log("[uw.js::setup] ActionHandler initiated:", this.actionHandler);
}
} catch (e) {
console.log("[uw::init] FAILED TO START EXTENSION. Error:", e);
}
@ -72,5 +98,5 @@ class UW {
}
}
var uw = new UW();
uw.init();
var main = new UW();
main.init();

BIN
src/icons/icon.xcf Normal file

Binary file not shown.

BIN
src/icons/icon_128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

BIN
src/icons/icon_48.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

59
src/manifest.json Normal file
View File

@ -0,0 +1,59 @@
{
"manifest_version": 2,
"name": "Ultrawidify",
"description": "Aspect ratio fixer for Youtube, Netflix and possibly more. Supports automatic aspect ratio detection on certain sites.",
"version": "4.0.1a1",
"applications": {
"gecko": {
"id": "{cf02b1a7-a01a-4e37-a609-516a283f1ed3}"
}
},
"icons": {
"32":"res/icons/uw-32.png",
"64":"res/icons/uw-64.png"
},
"browser_action": {
"default_title": "Ultrawidify",
"default_popup": "popup/popup.html"
},
"content_scripts": [{
"matches": ["*://*/*"],
"js": [
"ext/uw.js"
],
"all_frames": true
}],
"background": {
"scripts": [
"ext/uw-bg.js"
]
},
"options_ui": {
"page": "options/options.html",
"browser_style": false,
"open_in_tab": true
},
"web_accessible_resources": [
"./*",
"ext/*",
"res/fonts/*",
"res/css/*",
"res/img/ytplayer-icons/zoom.png",
"res/img/ytplayer-icons/unzoom.png",
"res/img/ytplayer-icons/fitw.png",
"res/img/ytplayer-icons/fith.png",
"res/img/ytplayer-icons/reset.png",
"res/img/ytplayer-icons/settings.png",
"res/img/settings/about-bg.png"
],
"permissions": [
"tabs", "storage", "activeTab", "<all_urls>", "webNavigation"
]
}

234
src/options/App.vue Normal file
View File

@ -0,0 +1,234 @@
<template>
<div>
<!-- POPUPS -->
<div v-if="anyOpenedPopups"
class="popup-container"
>
<AddEditActionPopup v-if="editActionPopupVisible"
:settings="settings"
:actionIndex="editActionIndex"
@close="closePopups()"
>
</AddEditActionPopup>
<ConfirmPopup v-if="confirmationDialogText"
:dialogText="confirmationDialogText"
@close="closePopups()"
@confirm="confirm()"
/>
</div>
<!-- ACTUAL PAGE -->
<div class="flex flex-row"
:class="{'blur': anyOpenedPopups}"
>
<div class="header flex flex-column">
<div class="flex extension-name text-sink-anchor">
<div class="text-sink title-sink-pad w100 text-center">
Ultrawidify
</div>
</div>
<!-- MENU ITEMS HERE -->
<div class="flex flex-column menu">
<div class="menu-item"
:class="{'selected-tab': selectedTab === 'general'}"
@click="setSelectedTab('general')"
>
General settings
</div>
<div class="menu-item"
:class="{'selected-tab': selectedTab === 'autoar'}"
@click="setSelectedTab('autoar')"
>
Autodetection
</div>
<div class="menu-item experimental"
:class="{'selected-tab': selectedTab === 'controls'}"
@click="setSelectedTab('controls')"
>
Actions
</div>
<div class="menu-item"
:class="{'selected-tab': selectedTab === 'txtconf'}"
@click="setSelectedTab('txtconf')"
>
Super advanced settings
</div>
<div class="menu-item"
:class="{'selected-tab': selectedTab === 'about'}"
@click="setSelectedTab('about')"
>
About
</div>
<div class="menu-item"
:class="{'selected-tab': selectedTab === 'donate'}"
@click="setSelectedTab('donate')"
>
Donate
</div>
</div>
</div>
<div class="flex content-area flex-column">
<div class="flex content-title text-sink-anchor">
<div class="text-sink title-sink-pad">
{{selectedTabTitle}}
</div>
</div>
<!-- MAIN PAGE HERE -->
<div class="content-content">
<GeneralSettings v-if="settingsInitialized && selectedTab === 'general'"
:settings="settings"
>
</GeneralSettings>
<AutodetectionSettings v-if="settingsInitialized && selectedTab === 'autoar'"
:settings="settings"
>
</AutodetectionSettings>
<ControlsSettings v-if="selectedTab === 'controls'"
:settings="settings"
@edit-event="showEditActionPopup($event)"
@remove-event="showRemoveActionPopup($event)"
>
</ControlsSettings>
<SuperAdvancedSettings v-if="selectedTab === 'txtconf'"
:settings="settings"
></SuperAdvancedSettings>
<About v-if="selectedTab === 'about'">
</About>
<Donate v-if="selectedTab === 'donate'" />
<!-- Vice City/beggathon reference: https://youtu.be/Mn3YEJTSYs8?t=770 -->
</div>
</div>
</div>
</div>
</template>
<script>
import Donate from '../common/misc/Donate.vue';
import SuperAdvancedSettings from './SuperAdvancedSettings.vue';
import Debug from '../ext/conf/Debug.js';
import BrowserDetect from '../ext/conf/BrowserDetect.js';
import ExtensionConf from '../ext/conf/ExtensionConf.js';
import ObjectCopy from '../ext/lib/ObjectCopy.js';
import Settings from '../ext/lib/Settings.js';
import GeneralSettings from './GeneralSettings';
import ControlsSettings from './controls-settings/ControlsSettings';
import AddEditActionPopup from './controls-settings/AddEditActionPopup';
import ConfirmPopup from './common/ConfirmationPopup';
import About from './about'
import AutodetectionSettings from './AutodetectionSettings';
// import SuperAdvancedSettings from './'
export default {
name: "Ultrawidify",
data () {
return {
selectedTab: "general",
selectedTabTitle: "General settings",
settings: {},
settingsInitialized: false,
editActionPopupVisible: false,
editActionIndex: -1,
anyOpenedPopups: false,
removeConfirmationDialog: '',
confirmationDialogText: '',
messages: {
removeAction: "Are you sure you want to remove this action?"
},
}
},
async created () {
this.settings = new Settings(undefined, this.updateSettings);
await this.settings.init();
this.settingsInitialized = true;
},
components: {
GeneralSettings,
ControlsSettings,
AddEditActionPopup,
About,
AutodetectionSettings,
ConfirmPopup,
SuperAdvancedSettings,
Donate,
},
methods: {
setSelectedTab(newTab) {
this.selectedTab = newTab;
if (newTab === 'general') {
this.selectedTabTitle = 'General settings';
} else if (newTab === 'autoar') {
this.selectedTabTitle = 'Advanced autodetection settings';
} else if (newTab === 'controls') {
this.selectedTabTitle = 'Actions';
} else if (newTab === 'txtconf') {
this.selectedTabTitle = 'Super advanced settings';
} else if (newTab === 'about') {
this.selectedTabTitle = 'About';
} else if (newTab === 'donate') {
this.selectedTabTitle = 'Beggathon';
}
},
updateSettings(newSettings) {
this.settings = newSettings;
this.settingsInitialized = true;
},
showEditActionPopup(event) {
this.editActionPopupVisible = true;
this.editActionIndex = event;
this.anyOpenedPopups = true;
},
showRemoveActionPopup(indexToRemove) {
this.editActionIndex = indexToRemove;
this.anyOpenedPopups = true;
this.confirmationDialogText = this.messages.removeAction;
},
closePopups(){
this.anyOpenedPopups = false;
this.editActionPopupVisible = false;
this.confirmationDialogText = '';
},
confirm(){
if (this.confirmationDialogText === this.messages.removeAction) {
this.settings.active.actions.splice(this.editActionIndex, 1);
this.settings.save();
}
this.closePopups();
}
}
};
</script>
<style src="../res/css/font/overpass.css"></style>
<style src="../res/css/font/overpass-mono.css"></style>
<style src="../res/css/flex.css"></style>
<style src="../res/css/common.scss"></style>
<style src="./options.scss"></style>
<style lang="scss" scoped>
.popup-container {
position: fixed;
width: 100%;
height: 100%;
z-index: 1000;
display: flex;
justify-content: center;
align-content: center;
}
.blur {
filter: blur(2px);
}
</style>

View File

@ -0,0 +1,680 @@
<template>
<div>
<h2>Quick settings</h2>
<div class="flex label-secondary">
How often should autodetection check for changes?
</div>
<div class="description">
Shorter intervals (left side of the slider) are more responsive to changes in aspect ratio detections,
but requires more system resources. Slider is logarithmic.
</div>
<div class="indent">
<div class="flex flex-row row-padding">
<div class="flex flex-input">
More often&nbsp;<small>(~60/s)</small>
<input type="range"
:value="Math.log(settings.active.arDetect.timers.playing)"
@change="setArCheckFrequency($event.target.value)"
min="2.3"
max="9.3"
step="any"
/>
&nbsp; Less often&nbsp;<small>(~1/10s)</small>
</div>
</div>
</div>
<div class="flex label-secondary">
How sensitive should aspect ratio detection be?
</div>
<div class="description">
Sensitive preset will allow the extension to correct aspect ratio even when letterbox isn't clearly defined. This
can result in aspect ratio detection correcting aspect ratio when it shouldn't.
Accurate preset will take a more conservative approach to determining aspect ratio, correcting aspect ratio only when
it's absolutely sure that the aspect ratio needs changing. This option results in fewer incorrect aspect ratio corrections,
but can also result in extension not correcting aspect ratio when it should.
Strict preset is 'accurate' on stereoids.
</div>
<div class="flex flex-row row-padding">
<Button label="Sensitive"
:selected="sensitivity === 'sensitive'"
@click.native="setConfirmationThresholds('sensitive')"
/>
<Button label="Balanced"
:selected="sensitivity === 'balanced'"
@click.native="setConfirmationThresholds('balanced')"
/>
<Button label="Accurate"
:selected="getSensitivity() === 'accurate'"
@click.native="setConfirmationThresholds('accurate')"
/>
<Button label="Strict"
:selected="getSensitivity() === 'strict'"
@click.native="setConfirmationThresholds('strict')"
/>
<Button label="Custom (changed below)"
:selected="getSensitivity() === 'user-defined'"
/>
</div>
<h2 style="padding-top: 150px">Autodetection settings in detail</h2>
<!-- <div>
<input type="checkbox"
v-model="showAdvancedOptions"
/>
Show advanced options
</div> -->
<div class="label">Autodetection frequency</div>
<div class="description">
Determines how often we check for aspect ratio changes.
More frequent aspect ratio checks can result in insane RAM usage.
Less frequent aspect ratio checks result in bigger delay between aspect ratio change and correction.
Delays are given in milliseconds.
</div>
<div class="info">Note that tick rate must be smaller than check frequency.
<!-- <a v-if="!showAdvancedOptions"
href="#"
@click="showAdvancedOptions = true"
>Show advanced options</a> -->
</div>
<div class="indent">
<div class="flex flex-row row-padding">
<div class="flex label-secondary form-label">
Frequency while playing:
</div>
<div class="flex flex-input">
<input type="text"
v-model="settings.active.arDetect.timers.playing"
/>
</div>
</div>
<div class="flex flex-row row-padding">
<div class="flex label-secondary form-label">
Frequency while paused:
</div>
<div class="flex flex-input">
<input type="text"
v-model="settings.active.arDetect.timers.paused"
/>
</div>
</div>
<div v-if="showAdvancedOptions" class="flex flex-row row-padding">
<div class="flex label-secondary form-label">
Error timeout:
</div>
<div class="flex flex-input">
<input type="text"
v-model="settings.active.arDetect.timers.error"
/>
</div>
</div>
</div>
<div class="indent">
<div class="flex flex-row row-padding" v-if="showAdvancedOptions">
<div class="flex label-secondary form-label">
Tick rate:
</div>
<div class="flex flex-input">
<input type="text"
v-model="settings.active.arDetect.timers.tickrate"
/>
</div>
</div>
</div>
<div class="label">Fallback mode</div>
<div class="description">
Some streaming sites implement stuff (DRM) that prevents us from detecting aspect ratio using our favourite way.
Fortunately for us, some browsers (most notably: Firefox) allow us to work around that limitation using an alternative.
Said alternative is not without downsides, though:<br />
<ul><li> it's less accurate and leaves a thin black edge all around the video</li>
<li> Uses bigger image sample, which translates into slightly to moderately increased RAM and CPU usage <small>(increase in resource usage depends on your monitor resolution)</small></li>
</ul>
This is why fallback mode can be toggled off separately from the main thing.
<span v-if="!fallbackModeAvailable">Unfortunately for you, <b>this option doesn't seem to be available in the browser you're using.</b></span>
</div>
<div class="indent">
<div class="flex flex-row row-padding">
<div class="flex label-secondary">
</div>
<div class="flex flex-input">
<input type="checkbox"
v-model="settings.active.arDetect.fallbackMode.enabled"
/> Enable fallback mode&nbsp;<span v-if="!fallbackModeAvailable">because I'm super duper sure I'm using firefox right now.</span>
</div>
</div>
<div v-if="showAdvancedOptions" class="flex flex-row row-padding">
<div class="flex label-secondary form-label">
Safety border thickness (in px)
</div>
<div class="flex flex-input">
<input type="text"
v-model="settings.active.arDetect.fallbackMode.safetyBorderPx"
/>
</div>
</div>
<div v-if="showAdvancedOptions" class="flex flex-row row-padding">
<div class="flex label-secondary form-label">
Don't react if detected edge is less than this many pixels thick:
</div>
<div class="flex flex-input">
<input type="text"
v-model="settings.active.arDetect.fallbackMode.noTriggerZonePx"
/>
</div>
</div>
</div>
<div class="label">Letterbox misallignment threshold</div>
<div class="description">
If top and bottom bar differ by more than this (0 0%, 1 100%), we do not correct aspect ratio.
</div>
<div class="indent">
<div class="flex flex-row row-padding">
<div class="flex label-secondary form-label">
Letterbox misalignment threshold
</div>
<div class="flex flex-input">
<input type="text"
v-model="settings.active.arDetect.allowedMisaligned"
/>
</div>
</div>
</div>
<div class="label">Sampling options</div>
<div class="description">
Various sampling related options.<br/>
<b>Static columns:</b> Image is sampled in this many columns, spaced at regular intervals between both edges.<br/>
<b>Random columns:</b> (ADVANCED; NOT IMPLEMENTED/PLANNED) In addition to static colums, sample image at this many random columns.<br/>
<b>Static rows:</b> (ADVANCED) Image is sampled in this many rows, spaced at regular intervals between both edges.<br/>
<b>Sample width, height:</b> (ADVANCED) size of the sample. Bigger -> more accurate aspect ratio detection, but uses more resources.<br/>
</div>
<div class="indent">
<div class="flex flex-row row-padding">
<div class="flex label-secondary form-label">
Static sample columns:
</div>
<div class="flex flex-input">
<input type="text"
v-model="settings.active.arDetect.sampling.staticCols"
/>
</div>
</div>
<div v-if="showAdvancedOptions" class="flex flex-row row-padding">
<div class="flex label-secondary form-label">
Random sample columns:
</div>
<div class="flex flex-input">
<input type="text"
v-model="settings.active.arDetect.sampling.randomCols"
/>
</div>
</div>
<div v-if="showAdvancedOptions" class="flex flex-row row-padding">
<div class="flex label-secondary form-label">
Static rows:
</div>
<div class="flex flex-input">
<input type="text"
v-model="settings.active.arDetect.sampling.staticRows"
/>
</div>
</div>
<div v-if="showAdvancedOptions" class="flex flex-row row-padding">
<div class="flex label-secondary form-label">
Sample width:
</div>
<div class="flex flex-input">
<input type="text"
v-model="settings.active.arDetect.canvasDimensions.sampleCanvas.width"
/>
</div>
</div>
<div v-if="showAdvancedOptions" class="flex flex-row row-padding">
<div class="flex label-secondary form-label">
Static rows:
</div>
<div class="flex flex-input">
<input type="text"
v-model="settings.active.arDetect.canvasDimensions.sampleCanvas.height"
/>
</div>
</div>
</div>
<div class="label">Blackbar</div>
<div class="description">
These settings determine what's considered black and what's considered non-black.<br/>
<b>Black level:</b> 0-255, where 0 is black. Anything below this number across all RGB components is considered black.
Black level can decrease if we detect darker blacks in the video. Lower values > more accurate edge detection;
higher values > detection is more forgiving to videos with less-than-ideal contrast ratios.<br/>
<b>Threshold:</b> If pixel is darker than the sum of black level and this value, it's considered black. In theory, lower -> better.
In practice, this value needs to be kept surprisingly high (8 might not be high enough), otherwise compression artifacts in videos
start having an adverse effect on quality of automatic detection.<br/>
<b>Gradient detection:</b> Attempt to discriminate between hard edges and gradients. 'Strict' and 'Lax' prevent aspect ratio
changes if we detected gradients instead of a legit edge. This results in fewer false positives, but may cause aspect ratio
detection to not work on darker frames.<br/>
<b>Image threshold:</b> When gradient detection is enabled, everything that's brighter than the sum of black level, threshold and
ths is considered to be non-black.<br/>
<b>Gradient threshold:</b> If the distance between last black pixel and the first non-black pixel in a given column is more than this value,
we're looking at a gradient. If this happens while gradient detection is on, we don't change aspect ratio.<br/>
<b>Gradient sample size:</b> This option is really only relevant when using 'lax' gradient detection. If we don't find a non-black pixel
within this distance after last known black pixel when scanning a column, we presume we're not on a gradient.
</div>
<div class="indent">
<div class="flex flex-row row-padding">
<div class="flex label-secondary form-label">
Black level:
</div>
<div class="flex flex-input">
<input type="text"
v-model="settings.active.arDetect.blackbar.blackLevel"
/>
</div>
</div>
<div class="flex flex-row row-padding">
<div class="flex label-secondary form-label">
Threshold:
</div>
<div class="flex flex-input">
<input type="text"
v-model="settings.active.arDetect.blackbar.threshold"
/>
</div>
</div>
<div class="flex flex-row row-padding">
<div class="flex label-secondary form-label">
Gradient detection:
</div>
<div class="flex flex-input">
<select v-model="settings.active.arDetect.blackbar.antiGradientMode">
<option :value="0">Disabled</option>
<option :value="1">Lax</option>
<option :value="2">Strict</option>
</select>
</div>
</div>
<div v-if="showAdvancedOptions" class="flex flex-row row-padding">
<div class="flex label-secondary form-label">
Image threshold
</div>
<div class="flex flex-input">
<input type="text"
v-model="settings.active.arDetect.blackbar.imageThreshold"
/>
</div>
</div>
<div v-if="showAdvancedOptions" class="flex flex-row row-padding">
<div class="flex label-secondary form-label">
Gradient threshold:
</div>
<div class="flex flex-input">
<input type="text"
v-model="settings.active.arDetect.blackbar.gradientThreshold"
/>
</div>
</div>
<div v-if="showAdvancedOptions" class="flex flex-row row-padding">
<div class="flex label-secondary form-label">
Gradient sample size:
</div>
<div class="flex flex-input">
<input type="text"
v-model="settings.active.arDetect.blackbar.gradientSampleSize"
/>
</div>
</div>
</div>
<div v-if="showAdvancedOptions">
<div class="label">Black frame detection</div>
<div class="description">
Black frame detection is a quick test that tries to determine whether we're looking at a black frame. This test prevents
us from wasting precious time trying to detect aspect ratio on frames that are too dark for reliable aspect ratio detection.<br/>
<b>Sample width, height:</b> Sample size. Since we're checking <i>every</i> pixel in this sample, dimensions should be kept small.<br/>
<b>Color variance treshold:</b> In videos (such as movie trailers), any text that appears on the black background is usually of
a roughly the same color, while dark movie footage is not. This allows us to trigger autodetection on dark movie footage and
to not trigger autodetection when movie trailer flashes some text on black background. If color variance is greater than this value,
blackframe detection will use 'lax' (lower) cummulative threshold to determine whether the frame is black or not. If color variance
is less than this value, 'strict' (higher) cummulative threshold will be used to determine whether the frame is black or not instead.<br/>
<b>Cumulative threshold:</b> If we add the maximum of red, green, blue values of every pixel in the sample and they total more than this, the frame is bright enough.
Comes in 'lax' and 'strict' versions. See 'color variance threshold' description for details about 'lax' and 'strict.'<br/>
<b>Black pixel threshold:</b> If more than this fraction of pixels from the sample are "black", we consider the frame black. This overrules cumulative threshold.
</div>
<div class="indent">
<div class="flex flex-row row-padding">
<div class="flex label-secondary form-label">
Blackframe sample width:
</div>
<div class="flex flex-input">
<input type="text"
v-model="settings.active.arDetect.canvasDimensions.blackframeCanvas.width"
/>
</div>
</div>
<div class="flex flex-row row-padding">
<div class="flex label-secondary form-label">
Blackframe sample height:
</div>
<div class="flex flex-input">
<input type="text"
v-model="settings.active.arDetect.canvasDimensions.blackframeCanvas.height"
/>
</div>
</div>
<div class="flex flex-row row-padding">
<div class="flex label-secondary form-label">
Color variance treshold:
</div>
<div class="flex flex-input">
<input type="text"
v-model="settings.active.arDetect.blackframe.sufficientColorVariance"
/>
</div>
</div>
<div class="flex flex-row row-padding">
<div class="flex label-secondary form-label">
Cumulative threshold (lax mode):
</div>
<div class="flex flex-input">
<input type="text"
v-model="settings.active.arDetect.blackframe.cumulativeThresholdLax"
/>
</div>
</div>
<div class="flex flex-row row-padding">
<div class="flex label-secondary form-label">
Cumulative threshold (strict mode):
</div>
<div class="flex flex-input">
<input type="text"
v-model="settings.active.arDetect.blackframe.cumulativeThresholdStrict"
/>
</div>
</div>
<div class="flex flex-row row-padding">
<div class="flex label-secondary form-label">
Black pixel threshold:
</div>
<div class="flex flex-input">
<input type=""
v-model="settings.active.arDetect.blackframe.blackPixelsCondition"
/>
</div>
</div>
</div>
</div>
<div class="label">Edge detection</div>
<div class="description">
Options in this section govern edge detection.<br/>
<b>Sample width</b> In a bid to detect "false" edges, we take two samples this many pixels wide near the point of our potential edge. One sample must be completely black, the other must contain a set
amount of non-black pixels.<br/>
<b>Detection threshold</b> non-black sample mentioned above needs to contain at least this many non-black pixels.<br/>
<b>Thickness quorum (per edge)</b> amount of samples that agree on the thincknes of the blackbar that we need in order to establish aspect ratio. Every edge needs to have at least this many. Values higher than {{~~(settings.active.arDetect.edgeDetection.singleSideConfirmationThreshold / 2)}} (quorum (total)/2) are pointless.<br/>
<b>Thickness quorum (total)</b> amount of samples that agree on the thinckess of the blackbar that we need in order to establish aspect ratio in case one of the edges doesn't contain enough samples to achieve quorum.<br/>
<b>Logo threshold</b> if edge candidate sits with count greater than this*all_samples, it can't be a logo or a watermark.<br/>
<b>Ignore middle area</b> When trying to detect area, ignore area between middle and canvasHeight * {this value} pixels towards the edge.<br/>
<b>Detect limit</b> stop search after finding a potential candidate in this many sample columns<br/>
</div>
<div class="indent">
<div class="flex flex-row row-padding">
<div class="flex label-secondary form-label">
Sample width:
</div>
<div class="flex flex-input">
<input type="text"
v-model="settings.active.arDetect.edgeDetection.sampleWidth"
/>
</div>
</div>
<div class="flex flex-row row-padding">
<div class="flex label-secondary form-label">
Detection threshold (px):
</div>
<div class="flex flex-input">
<input type="text"
v-model="settings.active.arDetect.edgeDetection.detectionThreshold"
/>
</div>
</div>
<div class="flex flex-row row-padding">
<div class="flex label-secondary form-label">
Thickness quorum (per edge):
</div>
<div class="flex flex-input">
<input
v-model="settings.active.arDetect.edgeDetection.confirmationThreshold"
/>
</div>
</div>
<div class="flex flex-row row-padding">
<div class="flex label-secondary form-label">
Thicnkess quorum (total):
</div>
<div class="flex flex-input">
<input
v-model="settings.active.arDetect.edgeDetection.singleSideConfirmationThreshold"
/>
</div>
</div>
<div class="flex flex-row row-padding" v-if="showAdvancedOptions">
<div class="flex label-secondary form-label">
Logo threshold:
</div>
<div class="flex flex-input">
<input
v-model="settings.active.arDetect.edgeDetection.logoThreshold"
/>
</div>
</div>
<div class="flex flex-row row-padding" v-if="showAdvancedOptions">
<div class="flex label-secondary form-label">
Ignore middle area:
</div>
<div class="flex flex-input">
<input
v-model="settings.active.arDetect.edgeDetection.middleIgnoredArea"
/>
</div>
</div>
<div class="flex flex-row row-padding" v-if="showAdvancedOptions">
<div class="flex label-secondary form-label">
Detect limit:
</div>
<div class="flex flex-input">
<input
v-model="settings.active.arDetect.edgeDetection.minColsForSearch"
/>
</div>
</div>
</div>
<div class="label">Guard line</div>
<div class="description">
Quick test to determine whether aspect ratio hasn't changed. Test is performed by taking two samples on each edge of the image
one in the last row of the letterbox (blackbar), and one in the first row of the video (image).<br/>
<b>Ignore edge margin:</b> We don't take blackbar and image samples {width * this} many pixels from left and right edge.<br/>
<b>Image threshold:</b> If all pixels in blackbar are black and this fraction (0-1) of pixels in image are non-black, we presume that aspect ratio hasn't changed.<br/>
<b>Edge tolerance (px):</b> I lied. Blackbar test happens this many pixels away from the last row of the letterbox.
</div>
<div class="indent">
<div class="flex flex-row row-padding">
<div class="flex label-secondary ">
</div>
<div class="flex flex-input">
<input type="checkbox"
v-model="settings.active.arDetect.guardLine.enabled"
/> Enable guardline</span>
</div>
</div>
<div class="flex flex-row row-padding">
<div class="flex label-secondary form-label">
Ignore edge margin:
</div>
<div class="flex flex-input">
<input type="text"
v-model="settings.active.arDetect.guardLine.ignoreEdgeMargin"
/>
</div>
</div>
<div class="flex flex-row row-padding">
<div class="flex label-secondary form-label">
Image threshold:
</div>
<div class="flex flex-input">
<input type="text"
v-model="settings.active.arDetect.guardLine.imageTestThreshold"
/>
</div>
</div>
<div class="flex flex-row row-padding">
<div class="flex label-secondary form-label">
Edge tolerance (px):
</div>
<div class="flex flex-input">
<input type="text"
v-model="settings.active.arDetect.guardLine.edgeTolerancePx"
/>
</div>
</div>
</div>
<div v-if="showAdvancedOptions">
<div class="label">Aspect ratio change threshold</div>
<div class="description">
New and old aspect ratio must differ by at least this much (%, 1=100%) before we trigger aspect ratio correction.
</div>
<div class="indent">
<div class="flex flex-row row-padding">
<div class="flex label-secondary form-label">
Aspect ratio change threshold.
</div>
<div class="flex flex-input">
<input type=""
v-model="settings.active.arDetect.allowedMisaligned"
/>
</div>
</div>
</div>
</div>
<div class="flex flex-row button-box sticky-bottom">
<Button label="Cancel"
@click.native="cancel()"
>
</Button>
<Button label="Save settings"
@click.native="saveManual()"
>
</Button>
</div>
</div>
</template>
<script>
import Button from '../common/components/Button.vue';
export default {
components: {
Button,
},
props: ['settings'],
data() {
return {
showAdvancedOptions: true,
fallbackModeAvailable: false,
sensitivity: 'sensitive',
}
},
created() {
this.sensitivity = this.getSensitivity();
const canvas = document.createElement('canvas');
canvas.width = 10;
canvas.height = 10;
const ctx = canvas.getContext('2d');
try {
ctx.drawWindow(window,0, 0, 10, 10, "rgba(0,0,0,0)");
this.fallbackModeAvailable = true;
} catch (e) {
// console.log("DrawWindow failed:", e)
this.fallbackModeAvailable = false;
}
},
methods: {
setArCheckFrequency(event) {
this.settings.active.arDetect.timers.playing = Math.floor(Math.pow(Math.E, event));
},
getSensitivity() {
if (this.settings.active.arDetect.edgeDetection.singleSideConfirmationThreshold === 3 && this.settings.active.arDetect.edgeDetection.confirmationThreshold === 1) {
return 'sensitive';
}
if (this.settings.active.arDetect.edgeDetection.singleSideConfirmationThreshold === 5 && this.settings.active.arDetect.edgeDetection.confirmationThreshold === 2) {
return 'balanced';
}
if (this.settings.active.arDetect.edgeDetection.singleSideConfirmationThreshold === 7 && this.settings.active.arDetect.edgeDetection.confirmationThreshold === 3) {
return 'accurate';
}
if (this.settings.active.arDetect.edgeDetection.singleSideConfirmationThreshold === 16 && this.settings.active.arDetect.edgeDetection.confirmationThreshold === 8) {
return 'strict';
}
return 'user-defined';
},
setConfirmationThresholds(sens) {
console.log("setting conf treshold", sens)
if (sens === 'sensitive') {
this.settings.active.arDetect.edgeDetection.singleSideConfirmationThreshold = 3;
this.settings.active.arDetect.edgeDetection.confirmationThreshold = 1;
} else if (sens === 'balanced') {
this.settings.active.arDetect.edgeDetection.singleSideConfirmationThreshold = 5;
this.settings.active.arDetect.edgeDetection.confirmationThreshold = 2;
} else if (sens === 'accurate') {
this.settings.active.arDetect.edgeDetection.singleSideConfirmationThreshold = 7;
this.settings.active.arDetect.edgeDetection.confirmationThreshold = 3;
} else if (sens === 'strict') {
this.settings.active.arDetect.edgeDetection.singleSideConfirmationThreshold = 16;
this.settings.active.arDetect.edgeDetection.confirmationThreshold = 8;
}
this.sensitivity = this.getSensitivity();
},
saveManual(){
this.settings.save();
// this.parsedSettings = JSON.stringify(this.settings.active, null, 2);
// this.lastSettings = JSON.parse(JSON.stringify(this.settings.active));
const ths = this;
this.$nextTick( () => {
ths.parsedSettings = JSON.stringify(ths.lastSettings, null, 2)
ths.lastSettings = JSON.parse(JSON.stringify(ths.settings.active))
});
},
cancel(){
this.parsedSettings = '';
this.settings.rollback();
const ths = this;
this.$nextTick( () => ths.parsedSettings = JSON.stringify(ths.lastSettings, null, 2) );
}
}
}
</script>
<style lang="scss" scoped>
.form-label {
width: 20rem;
text-align: right;
vertical-align: baseline;
}
</style>

View File

@ -0,0 +1,185 @@
<template>
<div>
<div class="label">
Enable this extension:
</div>
<div class="flex flex-row button-box">
<Button label="Always"
:selected="settings.active.sites['@global'].mode === ExtensionMode.Enabled"
@click.native="setDefaultExtensionMode(ExtensionMode.Enabled)"
>
</Button>
<Button label="On whitelisted sites"
:selected="settings.active.sites['@global'].mode === ExtensionMode.Whitelist"
@click.native="setDefaultExtensionMode(ExtensionMode.Whitelist)"
>
</Button>
<Button label="Never"
:selected="settings.active.sites['@global'].mode === ExtensionMode.Disabled"
@click.native="setDefaultExtensionMode(ExtensionMode.Disabled)"
>
</Button>
</div>
<div class="description">
<b>Always</b> enables this extension on every site you visit that you didn't blacklist.<br/>
<b>On whitelisted sites</b> enables this extension only on sites you explicitly whitelisted.<br/>
<b>Never</b> disables extension on all sites, even on those you whitelisted.
</div>
<div class="label">
Enable autodetection:
</div>
<div class="flex flex-row button-box">
<Button label="Always"
:selected="settings.active.sites['@global'].autoar === ExtensionMode.Enabled"
@click.native="setDefaultAutodetectionMode(ExtensionMode.Enabled)">
</Button>
<Button label="On whitelisted sites"
:selected="settings.active.sites['@global'].autoar === ExtensionMode.Whitelist"
@click.native="setDefaultAutodetectionMode(ExtensionMode.Whitelist)">
</Button>
<Button label="Never"
:selected="settings.active.sites['@global'].autoar === ExtensionMode.Disabled"
@click.native="setDefaultAutodetectionMode(ExtensionMode.Disabled)">
</Button>
</div>
<div class="description">
<b>Always</b> enables autodetection on every site this extension is enabled for, unless blacklisted.<br/>
<b>On whitelisted sites</b> enables autodetection only for sites that you explicitly enabled.<br/>
<b>Never</b> disables autodetection on all sites, even on those you whitelisted.<br/>
<!-- <br/> -->
<!-- For more settings related to autodetection, please check the 'Autodetection' tab. -->
</div>
<div class="label">
Default video alignment:
</div>
<div class="flex flex-row button-box">
<Button label="Left"
:selected="settings.active.sites['@global'].videoAlignment === VideoAlignment.Left"
@click.native="setDefaultvideoAlignment(VideoAlignment.Left)">
</Button>
<Button label="Center"
:selected="settings.active.sites['@global'].videoAlignment === VideoAlignment.Center"
@click.native="setDefaultvideoAlignment(VideoAlignment.Center)">
</Button>
<Button label="Right"
:selected="settings.active.sites['@global'].videoAlignment === VideoAlignment.Right"
@click.native="setDefaultvideoAlignment(VideoAlignment.Right)">
</Button>
</div>
<div class="label">
Default stretch mode:
</div>
<div class="flex flex-row button-box">
<Button label="Don't stretch"
:selected="settings.active.sites['@global'].stretch === Stretch.NoStretch"
@click.native="setDefaultStretchingMode(Stretch.NoStretch)">
</Button>
<Button label="Basic stretch"
:selected="settings.active.sites['@global'].stretch === Stretch.Basic"
@click.native="setDefaultStretchingMode(Stretch.Basic)">
</Button>
<Button label="Hybrid stretch"
:selected="settings.active.sites['@global'].stretch === Stretch.Hybrid"
@click.native="setDefaultStretchingMode(Stretch.Hybrid)">
</Button>
<Button label="Thin borders only"
:selected="settings.active.sites['@global'].stretch === Stretch.Conditional"
@click.native="setDefaultStretchingMode(Stretch.Conditional)"
>
</Button>
</div>
<div class="description">
<b>None:</b> do not stretch the video at all. This is the default option, for men of culture.<br/>
<b>Basic:</b> stretches video to fit the player or screen unconditionally. If video has letterbox encoded, this option <i>will not</i> try to remove letterbox before stretching. You probably shouldn't be using this option.<br/>
<b>Hybrid:</b> stretches the video to fit the player, but only if cropping didn't completely remove the black bars.<br/>
<b>Thin borders:</b> stretches only if the width of black borders after cropping is thin.
<br/>
Threshold for thin borders can be defined below.
</div>
<div class="indent">
<div class="flex flex-row row-padding">
<div class="flex label-secondary">
Thin border threshold:
</div>
<div class="flex flex-input">
<input type="number"
step="any"
:value="settings.active.stretch.conditionalDifferencePercent"
@input="updateStretchThreshold($event.target.value)"
>
</div>
</div>
</div>
<div class="label">
Reset settings
</div>
<div class="flex flex-row button-box">
<Button label="Reset settings"
@click.native="resetSettings()"
>
</Button>
</div>
</div>
</template>
<script>
import Button from '../common/components/Button';
import Stretch from '../common/enums/stretch.enum';
import ExtensionMode from '../common/enums/extension-mode.enum';
import VideoAlignment from '../common/enums/video-alignment.enum';
export default {
components: {
Button,
},
props: {
settings: Object
},
data () {
return {
Stretch: Stretch,
ExtensionMode: ExtensionMode,
VideoAlignment: VideoAlignment,
stretchThreshold: 0,
}
},
created () {
},
methods: {
setDefaultAutodetectionMode(mode) {
this.settings.active.sites['@global'].autoar = mode;
this.settings.save();
},
setDefaultExtensionMode(mode) {
this.settings.active.sites['@global'].mode = mode;
this.settings.save();
},
setDefaultvideoAlignment(mode) {
this.settings.active.sites['@global'].videoAlignment = mode;
this.settings.save();
},
setDefaultStretchingMode(mode) {
this.settings.active.sites['@global'].stretch = mode;
this.settings.save();
},
updateStretchThreshold(newThreshold) {
if (!newThreshold || isNaN(newThreshold)) {
return;
}
this.settings.active.stretch.conditionalDifferencePercent = newThreshold;
this.settings.save();
},
resetSettings() {
this.settings.active = JSON.parse(JSON.stringify(this.settings.default));
this.settings.save();
}
}
}
</script>

View File

@ -0,0 +1,100 @@
<template>
<div style="padding-bottom: 100px;">
<p>If you feel like you need an adventure, you can edit the settings the real manly way.</p>
<p>Features of editing settings via text:
<ul>
<li>Even less validation than in gui way</li>
<li>Includes settings that Tam hasn't (or won't) put into the GUI part of settings.</li>
<li>Absolutely NO hand holding!</li>
<li>Even less documentation (unless you go and crawl through source code on github)! Variable names are self-documenting (tm) enough</li>
</ul>
</p>
<p>Save button is all the way to the bottom.</p>
<div ref="settingsEditArea"
style="white-space: pre-wrap; border: 1px solid orange; padding: 10px;"
:class="{'jsonbg': !hasError, 'jsonbg-error': hasError}"
contenteditable="true"
@input="updateSettings"
>{{parsedSettings}}</div>
<div class="flex flex-row button-box sticky-bottom">
<Button label="Cancel"
@click.native="cancel()"
>
</Button>
<Button label="Save settings"
:disabled="hasError"
@click.native="saveManual()"
>
</Button>
</div>
</div>
</template>
<script>
import Button from '../common/components/Button.vue';
export default {
components: {
Button,
},
data() {
return {
hasError: false,
parsedSettings: '',
lastSettings: {},
}
},
created() {
this.parsedSettings = JSON.stringify(this.settings.active, null, 2);
this.lastSettings = JSON.parse(JSON.stringify(this.settings.active));
},
props: {
settings: Object,
},
computed: {
// parsedSettings() {
// return
// }
},
methods: {
updateSettings(val) {
try {
this.settings.active = JSON.parse(val.target.textContent);
this.hasError = false;
} catch (e) {
this.hasError = true;
}
},
saveManual(){
if (this.hasError) {
return;
}
this.settings.save();
// this.parsedSettings = JSON.stringify(this.settings.active, null, 2);
// this.lastSettings = JSON.parse(JSON.stringify(this.settings.active));
const ths = this;
this.$nextTick( () => {
ths.parsedSettings = JSON.stringify(ths.lastSettings, null, 2)
ths.lastSettings = JSON.parse(JSON.stringify(ths.settings.active))
});
},
cancel(){
this.parsedSettings = '';
this.settings.rollback();
const ths = this;
this.$nextTick( () => ths.parsedSettings = JSON.stringify(ths.lastSettings, null, 2) );
this.hasError = false;
}
}
}
</script>
<style scoped>
.jsonbg {
background-color: #131313;
}
.jsonbg-error {
background-color: #884420;
}
</style>

42
src/options/about.vue Normal file
View File

@ -0,0 +1,42 @@
<template>
<div>
<h2>Ultrawidify - an aspect ratio fixer for youtube <small>(and netflix and various other sites)</small></h2>
<p>Ultrawidify version: {{addonVersion}}. Created by Tamius Han (me).</p>
<p><b>Having an issue?</b> Report <strike>undocumented features</strike> bugs using one of the following options:
<ul>
<li> <a target="_blank" href="https://github.com/xternal7/ultrawidify/issues"><b>Github</b></a> <b>(strongly preferred)</b><br/></li>
<li>PM me on <a target="_blank" href="https://www.reddit.com/message/compose?to=xternal7&subject=[Ultrawidify]%20ENTER%20SUMMARY%20OF%20YOUR%20PROBLEM%20HERE&message=Describe+your+issue+in+more+detail.+Don%27t+forget+to+include%3A%0D%0A%2A+Extension+version%0D%0A%2A+Browser%0D%0A%2A+Operating+system%0D%0A%2A+Other+extensions+that+could+possibly+interfere%0D%0A%0D%0AIf+you%27re+reporting+an+issue+with+automatic+aspect+ratio+detection%2C+please+also+include+the+following+%28if+possible%29%3A%0D%0A%2A+model+and+make+of+your+CPU%0D%0A%2A+amount+of+RAM">reddit</a><br/></li>
<li>Email: <a target="_blank" href="mailto:tamius.han@gmail.com?subject=%5BUltrawidify%5D+ENTER+SUMMARY+OF+YOUR+ISSUE+HERE&body=Describe+your+issue+in+more+detail.+Don%27t+forget+to+include%3A%0D%0A%2A+Extension+version%0D%0A%2A+Browser%0D%0A%2A+Operating+system%0D%0A%2A+Other+extensions+that+could+possibly+interfere%0D%0A%0D%0AIf+you%27re+reporting+an+issue+with+automatic+aspect+ratio+detection%2C+please+also+include+the+following+%28if+possible%29%3A%0D%0A%2A+model+and+make+of+your+CPU%0D%0A%2A+amount+of+RAM">tamius.han@gmail.com</a></li>
</ul>
</p>
<p>&nbsp;</p>
<p>If you're curious about the source code, <a href="https://github.com/xternal7/ultrawidify">github's here</a>. It's available under sorta-you-can-look-but-you-can't-touch licence, meaning: you can look, I won't mind if you shoot me a pull request, but I will mind if you're just gonna reupload this extension to the AMO/Chrome store.</p>
<p>If you're looking at this page because you're bored and want to be bored some more, <a href="https://tamius.net">my website's here</a> and <a href="https://stuff.tamius.net/sacred-texts/">my blog is here</a>.</p>
<p>I already have a 'donation' tab, but if you want to buy me a beer, <a href="https://paypal.me/tamius">my paypal's here</a>.</p>
<h2>Plans for the future</h2>
<p>Improving automatic detection, trying to re-implement in-player user interface (confirmed!), fixing bugs. Maybe even give the settings page a facelift.</p>
<h2>Acknowledgements</h2>
<p>This extension uses font <a href="http://overpassfont.org/">Overpass</a> and everything <a href="https://github.com/Kocal/vue-web-extension">this Vue template brings along</a>.</p>
<h2>Special thanks to</h2>
<ul>
<li>Everyone who reports bugs</li>
<li>Everyone who waited for about 6-8 months for the 4.0 release. It was supposed to be released by the end of December 2018, we're in April. It was a major rewrite, though.</li>
<li>Everyone who donated at any point in time.</li>
<li>me for making this extension. You're welcome.</li>
</ul>
</div>
</template>
<script>
export default {
data() {
return {
addonVersion: browser.runtime.getManifest().version || chrome.runtime.getManifest().version,
}
}
}
</script>

View File

@ -0,0 +1,66 @@
<template>
<div class="full-screen"
@click="cancel()"
>
<div class="dialog-box flex flex-column" @click="$event.stopPropagation()">
<div>
{{dialogText}}
</div>
<div class="flex flex-row flex-end">
<ShortcutButton class="flex b3 button"
label="Ok"
@click.native="confirm()"
/>
<ShortcutButton class="flex b3 button"
label="Cancel"
@click.native="cancel()"
/>
</div>
</div>
</div>
</template>
<script>
import ShortcutButton from '../../common/components/ShortcutButton.vue'
export default {
props: {
dialogText: String,
},
components: {
ShortcutButton
},
methods: {
cancel() {
this.$emit('close')
},
confirm() {
this.$emit('confirm')
}
}
}
</script>
<style lang="scss" scoped>
.full-screen {
background-color: rgba(0,0,0,0.5);
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
position: absolute;
}
.dialog-box {
background-color: rgba(0,0,0,0.9);
// width: 100%;
// height: 100%;
max-width: 130rem;
max-height: 42rem;
padding-top: 2rem;
padding-left: 3rem;
padding-right: 3rem;
}
</style>

View File

@ -0,0 +1,329 @@
<template>
<div class="full-screen"
@click="cancel()"
>
<div class="dialog-box flex flex-column" @click="$event.stopPropagation()">
<div class="window-title">
{{actionIndex < 0 ? 'Add new action' : 'Edit action'}}
</div>
<div class="command-chain-border">
<CommandChain class="w100"
:command="action.cmd"
@new-command="addNewCommand()"
@edit="editCommand"
@delete="deleteCommand"
>
</CommandChain>
<CommandAddEdit class="w100 cae-margin"
v-if="addEditCommand"
:action="currentCmd"
@set-command="updateCommand"
@close-popup="addEditCommand=false"
>
</CommandAddEdit>
</div>
<div class="flex flex-column section-pad">
<div class="flex flex-row">
<div class="flex label-secondary form-label">
<span class="w100">
Action name:
</span>
</div>
<div class="flex flex-input flex-grow">
<input type="text"
class="w100"
:value="actionIndex < 0 ? action.name : settings.active.actions[actionIndex].name"
@input="updateActionName($event.target.value)"
>
</div>
</div>
<div class="flex flex-row">
<div class="flex label-secondary form-label">
<span class="w100">
Default label:
</span>
</div>
<div class="flex flex-input flex-grow">
<input type="text"
class="w100"
:value="actionIndex < 0 ? action.label : settings.active.actions[actionIndex].label"
@input="updateActionLabel($event.target.value)"
>
</div>
</div>
</div>
<div class="flex flex-row section-pad">
<b>Show this action in the following tabs:</b>
</div>
<template v-if="action && action.cmd[0] && action.cmd[0].action !== 'set-ar'">
<div class="tab-title">Extension settings (global)</div>
<ScopeSettings :scopeOptions="globalScopeOptions"
@show="updateScopes('global', 'show', $event)"
@set-label="updateScopes('global', 'label', $event)"
@set-shortcut="updateScopes('global', 'shortcut', $event)"
/>
</template>
<template v-if="action && action.cmd[0] && action.cmd[0].action !== 'set-ar'">
<div class="tab-title">Site settings (site)</div>
<ScopeSettings :scopeOptions="siteScopeOptions"
@show="updateScopes('site', 'show', $event)"
@set-label="updateScopes('site', 'label', $event)"
@set-shortcut="updateScopes('site', 'shortcut', $event)"
/>
</template>
<div class="tab-title">Video settings (page)</div>
<ScopeSettings :scopeOptions="pageScopeOptions"
@show="updateScopes('page', 'show', $event)"
@set-label="updateScopes('page', 'label', $event)"
@set-shortcut="updateScopes('page', 'shortcut', $event)"
/>
<div class="flex flex-row flex-end">
<ShortcutButton class="flex b3 button"
label="Save"
@click.native="saveSettings()"
/>
<ShortcutButton class="flex b3 button"
label="Cancel"
@click.native="cancel()"
/>
</div>
</div>
</div>
</template>
<script>
import ShortcutButton from '../../common/components/ShortcutButton.vue'
import Stretch from '../../common/enums/stretch.enum';
import KeyboardShortcutParser from '../../common/js/KeyboardShortcutParser';
import CommandChain from './command-builder/CommandChain';
import CommandAddEdit from './command-builder/CommandAddEdit';
import ScopeSettings from './scope-settings-component/ScopeSettings';
export default {
props: {
settings: Object,
actionIndex: Number,
},
components: {
CommandChain: CommandChain,
CommandAddEdit: CommandAddEdit,
ScopeSettings,
ShortcutButton
},
data () {
return {
Stretch: Stretch,
action: {
name: 'New action',
label: 'New action',
cmd: [],
scopes: {}
},
addEditCommand: false,
currentCmdIndex: -1,
}
},
created () {
if (this.actionIndex >= 0) {
// this.currentCmdIndex = this.actionIndex;
this.action = this.settings.active.actions[this.actionIndex];
this.currentCmdIndex = this.actionIndex;
}
},
computed: {
globalScopeOptions: function() {
return {
show: this.action.scopes.global && this.action.scopes.global.show || false,
label: (this.action.scopes.global && this.action.scopes.global.label) ? this.action.scopes.global.label : '',
shortcut: this.action.scopes.global && this.action.scopes.global.shortcut
}
},
siteScopeOptions: function() {
return {
show: this.action.scopes.site && this.action.scopes.site.show || false,
label: (this.action.scopes.site && this.action.scopes.site.label) ? this.action.scopes.site.label : '',
shortcut: this.action.scopes.site && this.action.scopes.site.shortcut
}
},
pageScopeOptions: function() {
return {
show: this.action.scopes.page && this.action.scopes.page.show || false,
label: (this.action.scopes.page && this.action.scopes.page.label) ? this.action.scopes.page.label : '',
shortcut: this.action.scopes.page && this.action.scopes.page.shortcut
}
}
},
watch: {
action: {
deep: true,
handler: function(val) {
this.action = val;
}
}
},
methods: {
parseActionShortcut(action) {
return KeyboardShortcutParser.parseShortcut(action.shortcut[0]);
},
parseCommand(cmd) {
let cmdstring = '';
for(const c of cmd) {
cmdstring += `${c.action} ${c.arg} ${c.customArg ? '' : c.customArg} | ${c.persistent ? ' persistent' : ''}; `;
}
return cmdstring;
},
updateActionName(newName) {
this.action.name = newName;
},
updateActionLabel(newLabel) {
this.action.label = newLabel;
},
updateScopes(scope, prop, value) {
if(this.action.scopes[scope] === undefined) {
this.action.scopes[scope] = {};
}
this.action.scopes[scope][prop] = value;
// TODO: remove for release
// this.action = JSON.parse(JSON.stringify(this.action))
},
addNewCommand() {
this.currentCmdIndex = -1;
this.currentCmd = undefined;
this.addEditCommand = true;
},
editCommand(index) {
this.currentCmdIndex = index;
this.currentCmd = this.action.cmd[index];
this.addEditCommand = true;
},
deleteCommand(index) {
this.action.cmd.splice(index,1);
},
updateCommand(action, arg, customArg) {
this.addEditCommand = false;
if (this.currentCmdIndex < 0) {
this.action.cmd.push({
action: action,
arg: arg,
customArg: customArg,
});
} else {
this.action.cmd[this.currentCmdIndex] = {
action: action,
arg: arg,
customArg: customArg,
};
}
this.action = JSON.parse(JSON.stringify(this.action));
},
saveSettings() {
if (this.currentCmdIndex < 0) {
this.settings.active.actions.push(this.action);
}
this.settings.save();
this.close();
},
cancel() {
this.settings.rollback();
this.close();
},
close() {
this.$emit('close');
}
}
}
</script>
<style lang="scss" scoped>
@import '../../res/css/colors';
.cae-margin {
margin-top: 20px;
}
.full-screen {
background-color: rgba(0,0,0,0.5);
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
position: absolute;
}
.dialog-box {
background-color: rgba(0,0,0,0.9);
// width: 100%;
// height: 100%;
max-width: 130rem;
max-height: 42rem;
padding-top: 2rem;
padding-left: 3rem;
padding-right: 3rem;
}
.window-title {
font-size: 2.4rem;
font-variant: small-caps;
margin-bottom: 1.69rem;
}
.form-label {
width: 16rem;
text-align: left;
// text-align: right;
vertical-align: baseline;
}
.tab-title {
font-size: 1.2em;
color: lighten($primary-color, 25%);
padding-top: 20px;
padding-bottom: 10px;
}
.hint {
opacity: 50% !important;
font-weight: 300;
}
.w100 {
width: 100%;
}
.button {
margin-left: 1em;
margin-right: 1em;
padding-left: 1em;
padding-right: 1em;
padding-top: 0.4em;
width: 4em;
text-align: center;
background-color: rgba(0,0,0,0.66);
color: #ffc;
height: 1.7em;
}
.command-chain-border {
border: 1px dotted lighten($selected-color, 10%);
padding: 10px;
padding-top: 20px;
padding-bottom: 20px;
}
.section-pad {
padding-top: 35px;
}
</style>

View File

@ -0,0 +1,164 @@
<template>
<div>
<p>Here you can create, edit or remove actions. This includes adding keyboard shortcuts that trigger them, or toggling visibility of each action in the extension popup.</p>
<p><b>Note that this section is highly experimental.</b> If there's extension-breaking bugs, please file a bug report on github or e-mail me.
However, there's some bugs that I'm aware of, but have little intentions to fix any time soon: <ul>
<li><b>Crop actions</b> (and maybe some others) are limited to the <i>page</i> scope (Video settings in popup) by design.
Yes, 'global' and 'site' checkboxes are still there, but they won't do anything. I probably won't remove them any time soon,
because I'd honestly rather spend my finite time on other things.</li>
<li>UI looks absolutely atrocious, and it's prolly gonna stay that way for a while. I am open to specific design suggestions, though.</li>
<li><b>For shortcuts:</b> you can use modifier keys (ctrl/alt/meta/shift), but remember to release modifier keys last. For example,
if you want to do use shift-S for anything: press both keys, release S, release shift. If you release shift first, extension
will think you were holding only shift key. You're prolly used to doing things that way already. If you're not bad news,
reworking how keyboard shortcuts work isn't going to happen for a while.</li>
<li>I don't think reordering (in a user-friendly way at least) is gonna happen any time soon.</li>
</ul>
</p>
<p>&nbsp;</p>
<div class="flex flex-row button-box">
<Button label="Add new action"
@click.native="addAction()"
>
</Button>
</div>
<div class="flex flex-column">
<div class="action-item-category-header">
Crop actions
</div>
<template v-for="(action, index) of settings.active.actions">
<ActionAlt v-if="action.cmd.length === 1 && action.cmd[0].action === 'set-ar'"
:key="index"
:action="action"
@edit="changeShortcut(index)"
@remove="removeAction(index)"
>
</ActionAlt>
</template>
<div class="action-item-category-header">
Stretch actions
</div>
<template v-for="(action, index) of settings.active.actions">
<ActionAlt v-if="action.cmd.length === 1 && action.cmd[0].action === 'set-stretch'"
:key="index"
:action="action"
@edit="changeShortcut(index)"
@remove="removeAction(index)"
>
</ActionAlt>
</template>
<div class="action-item-category-header">
Alignment actions
</div>
<template v-for="(action, index) of settings.active.actions">
<ActionAlt v-if="action.cmd.length === 1 && action.cmd[0].action === 'set-alignment'"
:key="index"
:action="action"
@edit="changeShortcut(index)"
@remove="removeAction(index)"
>
</ActionAlt>
</template>
<div class="action-item-category-header">
Zoom / panning actions
</div>
<template v-for="(action, index) of settings.active.actions">
<ActionAlt v-if="action.cmd.length === 1 && (
action.cmd[0].action === 'change-zoom' ||
action.cmd[0].action === 'set-zoom' ||
action.cmd[0].action === 'set-pan' ||
action.cmd[0].action === 'pan' ||
action.cmd[0].action === 'set-pan'
)"
:key="index"
:action="action"
@edit="changeShortcut(index)"
@remove="removeAction(index)"
>
</ActionAlt>
</template>
<div class="action-item-category-header">
Other actions
</div>
<template v-for="(action, index) of settings.active.actions">
<ActionAlt v-if="action.cmd.length > 1 || (
action.cmd[0].action !== 'change-zoom' &&
action.cmd[0].action !== 'set-zoom' &&
action.cmd[0].action !== 'set-pan' &&
action.cmd[0].action !== 'pan' &&
action.cmd[0].action !== 'set-pan' &&
action.cmd[0].action !== 'set-alignment' &&
action.cmd[0].action !== 'set-stretch' &&
action.cmd[0].action !== 'set-ar'
)"
:key="index"
:action="action"
@edit="changeShortcut(index)"
@remove="removeAction(index)"
>
</ActionAlt>
</template>
</div>
</div>
</template>
<script>
import Button from '../../common/components/Button';
import Stretch from '../../common/enums/stretch.enum';
import ActionAlt from '../../common/components/ActionAlt';
export default {
components: {
Button,
ActionAlt,
},
data () {
return {
Stretch: Stretch,
tableVisibility: {
crop: true,
}
}
},
created () {
},
props: {
settings: Object
},
watch: {
settings: (newVal, oldVal) => {
this.settings = newVal;
}
},
methods: {
removeAction(index) {
this.$emit('remove-event', index)
},
addAction() {
this.$emit('edit-event', -1);
},
changeShortcut(index) {
this.$emit('edit-event', index);
}
}
}
</script>
<style lang="scss" scoped>
@import '../../res/css/colors.scss';
.action-item-category-header {
margin-top: 3rem;
color: $primary-color;
font-size: 2.4rem;
font-variant: small-caps;
font-weight: 600;
}
</style>

View File

@ -0,0 +1,154 @@
<template>
<div class="">
<div class="window-content">
<!-- ACTION SELECT -->
<div class="flex flex-row">
<div class="flex label-secondary form-label">
<span class="w100">
Select action:
</span>
</div>
<div class="flex flex-grow flex-input">
<select class=""
:value="selectedAction"
@change="setAction($event.target.value)"
>
<option :value="undefined" selected disabled>Select ...</option>
<option v-for="(action, key) in ActionList"
:value="key"
:key="key"
>
{{action.name}}
</option>
</select>
</div>
</div>
<!-- ARGUMENT SELECT -->
<div v-if="selectedAction && ActionList[selectedAction]"
class="flex flex-row">
<div class="flex label-secondary form-label">
<span class="w100">
Select action parameter:
</span>
</div>
<div class="flex flex-grow flex-input">
<select class=""
:value="selectedArgument ? selectedArgument.arg : undefined"
@change="setArgument($event.target.value)"
>
<option :value="undefined" selected disabled>Select ...</option>
<option v-for="arg of ActionList[selectedAction].args"
:key="arg.arg"
:value="arg.arg"
>
{{arg.name}}
</option>
</select>
</div>
</div>
<!-- CUSTOM ARGUMENT INPUT -->
<div v-if="selectedArgument && selectedArgument.customArg"
class="flex flex-row">
<div class="flex label-secondary form-label">
<span class="w100">
{{selectedArgument.name}}:
</span>
</div>
<div class="flex flex-grow flex-input">
<input type="text"
class="w100"
v-model="customArgumentValue"
>
</div>
</div>
</div>
<div class="flex flex-row flex-end close-save-button-margin">
<div class="button"
@click="$emit('close-popup')"
>
Cancel
</div>
<div class="button"
@click="emitCommand()"
>
Save
</div>
</div>
</div>
</template>
<script>
import ActionList from '../../../ext/conf/ActionList';
import Stretch from '../../../common/enums/stretch.enum';
export default {
data () {
return {
Stretch: Stretch,
ActionList: ActionList,
selectedAction: undefined,
selectedArgument: undefined,
customArgumentValue: undefined
}
},
created () {
if (this.action) {
this.selectedAction = this.action.action;
this.selectedArgument = {
name: ActionList[this.action.action].args.find(x => x.arg === this.action.arg) || ActionList[this.action.action].args.find(x => x.customArg),
arg: this.action.arg
}
this.customArgumentValue = this.action.customArg;
}
// console.log("this.actionList", ActionList, this.ActionList)
// for(const a in ActionList) {
// console.log(a);
// }
},
mounted() {
},
props: {
action: Object,
scope: String,
},
methods: {
setAction(cmd) {
this.selectedAction = cmd;
this.selectedArgument = undefined;
this.customArgumentValue = undefined;
},
setArgument(arg) {
this.selectedArgument = ActionList[this.selectedAction].args.find(x => x.arg == arg);
this.customArgumentValue = undefined;
},
emitCommand() {
this.$emit(
'set-command',
this.selectedAction,
this.selectedArgument.arg,
this.customArgumentValue
);
}
}
}
</script>
<style>
.form-label {
width: 15rem;
text-align: left;
/* text-align: right; */
vertical-align: baseline;
}
.close-save-button-margin {
padding-top: 25px;
}
</style>

View File

@ -0,0 +1,103 @@
<template>
<div class="block-main">
<div class="button-main">
<div class="button-action-display">
{{ActionList[action.action].name}}:&nbsp;{{
(ActionList[action.action].args.find(x => x.arg === action.arg) || action).arg
}}
</div>
<div class="actions flex flex-row flex-center flex-cross-center">
<div v-if="!first || true"
class="flex order-button"
@click="$emit('move-left')"
>
&lt;
</div>
<div class="flex flex-grow command-action"
@click="$emit('delete')"
>
🗙
</div>
<div class="flex flex-grow command-action"
@click="$emit('edit')"
>
🖉
</div>
<div v-if="!last || true"
class="flex order-button"
@click="$emit('move-right')"
>
&gt;
</div>
</div>
</div>
</div>
</template>
<script>
import ActionList from '../../../ext/conf/ActionList';
export default {
data () {
return {
ActionList: ActionList,
}
},
created () {
},
props: {
action: Object,
first: Boolean,
last: Boolean
},
methods: {
parseActionShortcut(action) {
if (action.shortcut) {
return KeyboardShortcutParser.parseShortcut(action.shortcut[0]);
}
},
setAction(cmd) {
this.selectedAction = cmd;
this.selectedArgument = undefined;
this.customArgumentValue = undefined;
},
setArgument(arg) {
this.selectedArgument = arg;
this.customArgumentValue = undefined;
},
// emitCommand() {
// this.$emit(
// 'set-command',
// this.selectedAction,
// this.customArgumentValue ? this.customArgumentValue : this.selectedArgument.arg
// );
// }
}
}
</script>
<style lang="scss" scoped>
@import './../../../res/css/colors';
.block-main {
border: 1px solid $primary-color;
min-width: 10rem;
padding-top: 0.1rem;
margin-left: 0.5rem;
margin-right: 0.5rem;
}
.button-action-display, .command-action, .order-button {
display: block;
padding-left: 1rem;
padding-right: 1rem;
text-align: center;
}
.button-action-display {
padding-top: 0.1rem;
}
</style>

View File

@ -0,0 +1,51 @@
<template>
<div class="command-chain flex flex-row flex-center flex-cross-center">
<!-- {{command}} -->
<div class="flex flex-row flex-center flex-cross-center">
<CommandBlock v-for="(cmd, index) of command"
:key="index"
:action="cmd"
@edit="$emit('edit', index)"
@delete="$emit('delete', index)"
@move-left="$emit('move-left', index)"
@move-right="$emit('move-left', (index + 1))"
>
</CommandBlock>
<div class="new-command h100 flex flex-column flex-center flex-cross-center"
@click="$emit('new-command')"
>
<div><b>+</b></div>
</div>
</div>
</div>
</template>
<script>
import CommandBlock from './CommandBlock';
import CommandAddEdit from './CommandAddEdit';
export default {
created () {
},
components: {
CommandBlock,
CommandAddEdit,
},
props: [
'command'
],
methods: {
}
}
</script>
<style lang="scss" scoped>
@import '../../../res/css/colors';
.new-command {
color: $primary-color;
height: 100%;
padding-left: 2rem;
padding-right: 2rem;
font-size: 2rem;
}
</style>

View File

@ -0,0 +1,65 @@
<template>
<div>
<div class="flex flex-row">
<div class="flex label-secondary form-label">
Show in popup:
</div>
<div class="flex flex-input flex-grow">
<input type="checkbox"
:checked="scopeOptions.show"
@input="$emit('show', $event.target.checked)"
/>
</div>
</div>
<div class="flex flex-row">
<div class="flex label-secondary form-label">
Custom label <small>(optional)</small>:
</div>
<div class="flex flex-input flex-grow">
<input type="text"
v-model="scopeOptions.label"
@input="setLabel($event.target.value)"
/>
</div>
</div>
<div class="flex flex-row">
<div class="flex label-secondary form-label">
<span class="w100">
Shortcut:
</span>
</div>
<div class="flex flex-input flex-grow">
<SetShortcutButton
:shortcut="scopeOptions.shortcut"
@set-shortcut="$emit('set-shortcut', $event)"
/>
</div>
</div>
</div>
</template>
<script>
import SetShortcutButton from './SetShortcutButton.vue';
export default {
components: {
SetShortcutButton
},
props: {
scopeOptions: Object,
},
methods: {
setLabel(label) {
if (label.trim() === '') {
label = undefined;
}
this.$emit('set-label', label.trim())
}
}
}
</script>
<style>
</style>

View File

@ -0,0 +1,65 @@
<template>
<div class="w100">
<input type="text"
class="shortcut-change-input"
ref="input"
:value="shortcutText"
@focus="initiateKeypress"
@keyup="processKeyup"
/>
<span v-if="shortcut" @click="$emit('set-shortcut')">(Clear shortcut)</span>
</div>
</template>
<script>
import KeyboardShortcutParser from '../../../common/js/KeyboardShortcutParser'
export default {
props: {
shortcut: Array, // note: array in unlikely case we ever try to implement choords
waitingForPress: false,
},
data() {
return {
shortcutText: '[click to add shortcut]'
}
},
created(){
if (this.shortcut && this.shortcut.length) {
this.shortcutText = KeyboardShortcutParser.parseShortcut(this.shortcut[0]);
}
},
methods: {
initiateKeypress() {
this.shortcutText = '[Press a key or key combination]';
this.waitingForPress = true;
},
processKeyup(event) {
if (this.waitingForPress) {
const shortcut = {
key: event.key,
ctrlKey: event.ctrlKey,
metaKey: event.metaKey,
altKey: event.altKey,
shiftKey: event.shiftKey,
onKeyUp: true,
onKeyDown: false,
};
this.$emit('set-shortcut', [shortcut])
this.$refs.input.blur();
this.shortcutText = KeyboardShortcutParser.parseShortcut(shortcut);
}
this.waitingForPress = false;
}
}
}
</script>
<style style="scss" scoped>
.shortcut-change-input {
background-color: transparent;
border: 0px solid transparent;
text-align: center;
width: 100%;
}
</style>

15
src/options/options.html Normal file
View File

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Ultravidify - Options</title>
<link rel="stylesheet" href="options.css">
<% if (NODE_ENV === 'development') { %>
<!-- Load some resources only in development environment -->
<% } %>
</head>
<body>
<div id="app"></div>
<script src="options.js"></script>
</body>
</html>

11
src/options/options.js Normal file
View File

@ -0,0 +1,11 @@
// console.log("global browser", browser, global.browser)
// global.browser = require('webextension-polyfill')
import Vue from 'vue'
import App from './App'
/* eslint-disable no-new */
new Vue({
el: '#app',
render: h => h(App)
})

249
src/options/options.scss Normal file
View File

@ -0,0 +1,249 @@
@import "../res/css/font/overpass.css";
@import "../res/css/font/overpass-mono.css";
@import "../res/css/colors.scss";
@import "../res/css/common.scss";
body {
background-image: url('/res/img/settings/bg_random2.png');
background-size: 25em;
text-align: center;
z-index: -1000;
}
/*
+
+ BASIC LAYOUT
+
*/
.header {
width: 42rem;
height: 100vh;
position: fixed;
}
.content-area {
width: 100%;
padding-left: 42rem;
text-align: center;
}
.text-sink-anchor {
position: relative;
}
.text-sink {
position: absolute;
bottom: 0px;
}
.title-sink-pad {
padding-bottom: 1.69rem;
}
/*
+
+ SIDE MENU
+
*/
.extension-name {
font-size: 4.20rem;
height: 7.7rem;
text-align: center;
}
.menu {
font-size: 1.9rem;
font-variant: small-caps;
}
/*
+
+ TAB PANEL
+
*/
.content-title {
font-size: 3.3rem;
height: 7.7rem;
}
.content-content {
width: 100%;
max-width: 96rem;
display: inline-block;
margin: 0 auto;
text-align: left;
}
.block {
display: inline-block;
margin-left: 1em;
margin-right: 1em;
}
.tabline {
background-color: #000;
width: 100%;
margin-bottom: 1.5em;
padding-top: 0.3em;
padding-bottom: 0.4em;
z-index: -200;
}
.dup_keybinds{
background-color: #720 !important;
}
.uw_shortcuts_line{
padding-top: 0.25em;
padding-left: 5em;
}
.uw_shortcuts_label{
display: inline-block;
color: #fff;
width: 17.5em;
}
.uw_options_line{
margin-top: 0.75em;
font-size: 1.1em;
width: 80%;
margin-left: 5%;
}
.uw_options_option{
margin-left: 5%;
}
.uw_suboption{
margin-left: 5em;
margin-top: 0.75em;
font-size: 0.85em;
}
.uw_options_desc, .uw_suboption_desc{
margin-top: 0.33em;
font-size: 0.69em;
color: #aaa;
}
.uw_suboption_desc{
margin-left: 5em;
}
.buttonbar{
/* width: 100%; */
padding-left: 20em;
margin-bottom: 2em;
}
.button {
display: inline-block;
margin-left: 1em;
margin-right: 1em;
padding-left: 1em;
padding-right: 1em;
padding-top: 0.4em;
width: 4em;
text-align: center;
background-color: rgba(0,0,0,0.66);
color: #ffc;
height: 1.7em;
}
.monospace {
font-family: 'Overpass Mono', monospace
}
.center-text {
text-align: center;
}
/** site options css **/
.site_name {
padding-left: 1em;
padding-bottom: 0.3em;
color: #fff;
font-size: 1.1em;
height: 13em !important;
}
.site_details {
font-size: 0.8em;
}
.site_title_ebox {
width: 10em !important;
font-size: 1.5em !important;
background-color: rgba(0,0,0,0) !important;
margin-left: 0px !important;
border: 0px !important;
color: #ffc !important;
}
.details_ebox {
width: 12em !important;
background-color: rgba(0,0,0,0) !important;
border: 0px !important;
color: #ffc !important;
margin-left: 0em !important;
}
.details_ebox:disabled {
color: #aaa !important;
}
.checkbox-center {
text-align: center;
}
.checkbox-container {
display: inline-block;
position: relative;
padding-left: 35px;
margin-bottom: 12px;
/* cursor: pointer; */
font-size: 22px;
/* -webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none; */
}
.checkbox {
position: absolute;
top: 0;
left: 0;
height: 25px;
width: 25px;
}
.checkbox-checked {
position: absolute;
left: 9px;
top: 5px;
width: 5px;
height: 10px;
border: solid rgb(245, 145, 63);
border-width: 0 3px 3px 0;
transform: rotate(45deg);
}
/* ACTION TABLE STYLES */
.action-item-category-label {
color: rgb(245, 145, 63);
font-size: 2em;
font-weight: 200;
margin-bottom: 0px;
padding-bottom: 0px;
padding-top: 1em;
}
.action-item-table-header {
color: rgb(245, 145, 63);
}
.action-item-table-header-small {
color: rgb(245, 145, 63);
font-size: 0.85em !important;
font-weight: 300 !important;
}
.action-list-item:hover {
background-color: rgba(254, 146, 63, 0.15);
}

377
src/popup/App.vue Normal file
View File

@ -0,0 +1,377 @@
<template>
<div class="popup">
<div class="header">
<span class="smallcaps">Ultrawidify</span>: <small>Quick settings</small>
</div>
<div class="flex flex-row">
<!-- TABS/SIDEBAR -->
<div id="tablist" class="flex flex-column flex-nogrow flex-noshrink">
<div class="menu-item"
:class="{'selected-tab': selectedTab === 'global'}"
@click="selectTab('global')"
>
<div class="">
Extension settings
</div>
<div class="">
</div>
</div>
<div class="menu-item"
:class="{'selected-tab': selectedTab === 'site'}"
@click="selectTab('site')"
>
<div class="">
Site settings
</div>
<div class="">
</div>
</div>
<div class="menu-item"
:class="{'selected-tab': selectedTab === 'video'}"
@click="selectTab('video')"
>
<div class="">
Video settings
</div>
<div v-if="selectedTab === 'video' && this.activeFrames.length > 0"
class=""
>
<small>Select embedded frame to control:</small>
<div class="">
<div v-for="frame of activeFrames"
class="tabitem"
:class="{'tabitem-selected': selectedFrame === frame.id}"
:key="frame.id"
@click="selectFrame(frame.id)"
>
{{frame.label}} <span v-if="frame.name !== undefined && frame.color" :style="{'background-color': frame.color}">[{{frame.name}}]</span>
</div>
</div>
</div>
</div>
<div class="menu-item"
:class="{'selected-tab': selectedTab === 'about'}"
@click="selectTab('about')"
>
<div class="">
About
</div>
<div class="">
</div>
</div>
<div class="menu-item"
:class="{'selected-tab': selectedTab === 'donate'}"
@click="selectTab('donate')"
>
<div class="">
Donate
</div>
<div class="">
</div>
</div>
</div>
<!-- PANELS/CONTENT -->
<div id="tab-content" class="flex-grow" style="max-width: 480px !important;">
<VideoPanel v-if="settings && settings.active && selectedTab === 'video'"
class=""
:settings="settings"
:frame="selectedFrame"
:zoom="currentZoom"
@zoom-change="updateZoom($event)"
/>
<DefaultSettingsPanel v-if="settings && settings.active && (selectedTab === 'site' || selectedTab === 'global')"
class=""
:settings="settings"
:scope="selectedTab"
:site="site && site.host"
/>
<PerformancePanel v-if="selectedTab === 'performance-metrics'"
:performance="performance" />
<AboutPanel v-if="selectedTab === 'about'" />
<Donate v-if="selectedTab === 'donate'" />
</div>
</div>
</div>
</template>
<script>
import Donate from '../common/misc/Donate.vue';
import Debug from '../ext/conf/Debug';
import BrowserDetect from '../ext/conf/BrowserDetect';
import Comms from '../ext/lib/comms/Comms';
import VideoPanel from './panels/VideoPanel';
import PerformancePanel from './panels/PerformancePanel';
import Settings from '../ext/lib/Settings';
import ExecAction from './js/ExecAction.js';
import DefaultSettingsPanel from './panels/DefaultSettingsPanel';
import AboutPanel from './panels/AboutPanel';
export default {
data () {
return {
selectedTab: 'video',
selectedFrame: '__all',
activeFrames: [],
port: BrowserDetect.firefox ? browser.runtime.connect({name: 'popup-port'}) : chrome.runtime.connect({name: 'popup-port'}),
comms: new Comms(),
frameStore: {},
frameStoreCount: 0,
performance: {},
site: null,
currentZoom: 1,
execAction: new ExecAction(),
settings: new Settings(undefined, () => this.updateConfig()),
}
},
async created() {
await this.settings.init();
this.port.onMessage.addListener( (m,p) => this.processReceivedMessage(m,p));
this.execAction.setSettings(this.settings);
// ensure we'll clean player markings on popup close
window.addEventListener("unload", () => {
this.port.postMessage({
cmd: 'unmark-player',
forwardToAll: true,
});
});
// get info about current site from background script
while (true) {
try {
this.getSite();
} catch (e) {
}
await this.sleep(5000);
}
},
components: {
VideoPanel,
DefaultSettingsPanel,
PerformancePanel,
Debug,
AboutPanel,
Donate,
},
methods: {
async sleep(t) {
return new Promise( (resolve,reject) => {
setTimeout(() => resolve(), t);
});
},
toObject(obj) {
return JSON.parse(JSON.stringify(obj));
},
getSite() {
try {
if (Debug.debug) {
console.log("[popup.js] requesting current site");
}
this.port.postMessage({cmd: 'get-current-site'});
} catch (e) {
if (Debug.debug) {
console.log("[popup::getSite] sending get-current-site failed for some reason. Reason:", e)
}
}
},
getRandomColor() {
return `rgb(${Math.floor(Math.random() * 128)}, ${Math.floor(Math.random() * 128)}, ${Math.floor(Math.random() * 128)})`;
},
selectTab(tab) {
this.selectedTab = tab;
},
selectFrame(frame) {
this.selectedFrame = frame;
},
async updateConfig() {
},
processReceivedMessage(message, port) {
if (Debug.debug) {
console.log("[popup.js] received message set-c", message);
console.log("[popup.js] message cloned set-c", JSON.parse(JSON.stringify(message)));
}
if(message.cmd === 'set-current-site'){
if (this.site) {
if (!this.site.host) {
// dunno why this fix is needed, but sometimes it is
this.site.host = site.tabHostname;
}
}
if (!this.site || this.site.host !== message.site.host) {
this.port.postMessage({cmd: 'get-current-zoom'});
}
this.site = message.site;
// loadConfig(site.host); TODO
this.loadFrames(this.site);
} else if (message.cmd === 'set-current-zoom') {
this.setCurrentZoom(message.zoom);
} else if (message.cmd === 'performance-update') {
for (let key in message.message) {
this.performance[key] = message.message[key];
}
}
return true;
},
loadFrames(videoTab) {
if (videoTab.selected) {
this.selectedSubitem = videoTab.selected;
// selectedSubitemLoaded = true;
}
this.activeFrames = [];
if (videoTab.frames.length < 2 || Object.keys(videoTab.frames).length < 2) {
this.selectedFrame = '__all';
return;
}
for (const frame in videoTab.frames) {
if (frame && !this.frameStore[frame]) {
const fs = {
name: this.frameStoreCount++,
color: this.getRandomColor()
}
this.frameStore[frame] = fs;
this.port.postMessage({
cmd: 'mark-player',
forwardToContentScript: true,
targetTab: videoTab.id,
targetFrame: frame,
name: fs.name,
color: fs.color
});
}
}
this.activeFrames = [{id: '__all', label: 'All'},{id: '__playing', label: 'Currently playing'}];
for (const frame in videoTab.frames) {
this.activeFrames.push({
id: `${this.site.id}-${frame}`,
label: videoTab.frames[frame].host,
...this.frameStore[frame],
})
}
},
getRandomColor() {
return `rgb(${Math.floor(Math.random() * 128)}, ${Math.floor(Math.random() * 128)}, ${Math.floor(Math.random() * 128)})`;
},
setCurrentZoom(nz) {
this.currentZoom = nz;
},
updateZoom(nz){
this.currentZoom = nz;
},
selectFrame(id){
this.selectedFrame = id;
}
}
}
</script>
<style src="../res/css/font/overpass.css"></style>
<style src="../res/css/font/overpass-mono.css"></style>
<style src="../res/css/flex.css"></style>
<style src="../res/css/common.scss"></style>
<style lang="scss" scoped>
html, body {
width: 800px !important;
max-width: 800px !important;
padding: 0px;
margin: 0px;
}
#tablist {
min-width: 275px;
}
.header {
overflow: hidden;
background-color: #7f1416;
color: #fff;
margin: 0px;
margin-top: 0px;
padding-top: 8px;
padding-left: 15px;
padding-bottom: 1px;
font-size: 2.7em;
}
.menu-item-inline-desc{
font-size: 0.60em;
font-weight: 300;
font-variant: normal;
}
.menu-item {
padding-left: 15px;
padding-top: 5px;
padding-bottom: 5px;
font-variant: small-caps;
border-left: transparent 5px solid;
}
.suboption {
display: block;
padding-left: 15px;
padding-right: 15px;
padding-top: 5px;
padding-bottom: 20px;
min-height: 250px;
}
#no-videos-display {
height: 100%;
padding-top: 50px;
/* text-align: center; */
}
.tabitem-container {
padding-top: 0.5em;
}
.selected-tab {
background-color: initial;
border-left: #f18810 5px solid;
}
.tabitem {
font-variant: normal;
// font-size: 0.69em;
margin-left: 1em;
border-left: transparent 3px solid;
padding-left: 12px;
margin-left: -10px;
}
.tabitem-selected {
color: #fff !important;
background-color: initial;
border-left: #f0c089 3px solid !important;
}
.tabitem-selected::before {
padding-right: 0.5em;
}
.tabitem-iframe::after {
content: "</>";
padding-left: 0.33em;
}
.popup {
max-width: 780px;
// width: 800px;
height: 600px;
}
</style>

View File

@ -0,0 +1,58 @@
import Comms from '../../ext/lib/comms/Comms';
class ExecAction {
constructor(settings, site) {
this.settings = settings;
this.site = site;
}
setSettings(settings) {
this.settings = settings;
}
setSite(site) {
this.site = site;
}
exec(action, scope, frame) {
for (var cmd of action.cmd) {
if (scope === 'page') {
const message = {
forwardToContentScript: true,
targetFrame: frame,
frame: frame,
cmd: cmd.action,
arg: cmd.arg,
customArg: cmd.customArg
}
Comms.sendMessage(message);
} else {
let site = this.site;
if (scope === 'global') {
site = '@global';
} else if (!this.site) {
site = window.location.host;
}
if (scope === 'site' && !this.settings.active.sites[site]) {
this.settings.active.sites[site] = this.settings.getDefaultOption();
}
if (cmd.action === "set-stretch") {
this.settings.active.sites[site].stretch = cmd.arg;
} else if (cmd.action === "set-alignment") {
this.settings.active.sites[site].videoAlignment = cmd.arg;
} else if (cmd.action === "set-extension-mode") {
this.settings.active.sites[site].mode = cmd.arg;
} else if (cmd.action === "set-autoar-mode") {
this.settings.active.sites[site].autoar = cmd.arg;
} else if (cmd.action === 'set-keyboard') {
this.settings.active.sites[site].keyboardShortcutsEnabled = cmd.arg;
}
this.settings.save();
}
}
}
}
export default ExecAction;

View File

@ -0,0 +1,25 @@
<template>
<div>
<div class="row">
<span class="label">Ultrawidify version:</span><br/> {{addonVersion}}
</div>
<div class="row">
<span class="label">Having an issue?</span><br/> Report <strike>undocumented features</strike> bugs using one of the following options:
<ul>
<li> <a target="_blank" href="https://github.com/xternal7/ultrawidify/issues"><b>Github (strongly preferred)</b></a><br/></li>
<li>PM me on <a target="_blank" href="https://www.reddit.com/message/compose?to=xternal7&subject=[Ultrawidify]%20ENTER%20SUMMARY%20OF%20YOUR%20PROBLEM%20HERE&message=Describe+your+issue+in+more+detail.+Don%27t+forget+to+include%3A%0D%0A%2A+Extension+version%0D%0A%2A+Browser%0D%0A%2A+Operating+system%0D%0A%2A+Other+extensions+that+could+possibly+interfere%0D%0A%0D%0AIf+you%27re+reporting+an+issue+with+automatic+aspect+ratio+detection%2C+please+also+include+the+following+%28if+possible%29%3A%0D%0A%2A+model+and+make+of+your+CPU%0D%0A%2A+amount+of+RAM">reddit</a><br/></li>
<li>Email: <a target="_blank" href="mailto:tamius.han@gmail.com?subject=%5BUltrawidify%5D+ENTER+SUMMARY+OF+YOUR+ISSUE+HERE&body=Describe+your+issue+in+more+detail.+Don%27t+forget+to+include%3A%0D%0A%2A+Extension+version%0D%0A%2A+Browser%0D%0A%2A+Operating+system%0D%0A%2A+Other+extensions+that+could+possibly+interfere%0D%0A%0D%0AIf+you%27re+reporting+an+issue+with+automatic+aspect+ratio+detection%2C+please+also+include+the+following+%28if+possible%29%3A%0D%0A%2A+model+and+make+of+your+CPU%0D%0A%2A+amount+of+RAM">tamius.han@gmail.com</a></li>
</ul>
</div>
</div>
</template>
<script>
export default {
data() {
return {
addonVersion: browser.runtime.getManifest().version || chrome.runtime.getManifest().version,
}
}
}
</script>

View File

@ -0,0 +1,185 @@
<template>
<div class="w100 flex flex-column" style="padding-bottom: 20px">
<!-- <ShortcutButton class="button"
@click.native="wipeSettings()"
label="Wipe settings"
/> -->
<!-- ENABLE EXTENSION -->
<div v-if="settings && extensionActions.length"
class="w100"
>
<div class="label">Enable extension {{scope === 'site' ? 'for this site' : ''}}:</div>
<div class="flex flex-row flex-wrap">
<ShortcutButton v-for="(action, index) of extensionActions"
class="flex flex-grow button"
:key="index"
:class="{'setting-selected': getCurrent('mode') === action.cmd[0].arg }"
:label="(action.scopes[scope] && action.scopes[scope].label) ? action.scopes[scope].label : action.label"
:shortcut="parseShortcut(action)"
@click.native="execAction(action)"
>
</ShortcutButton>
</div>
</div>
<!-- ENABLE AUTODETECTION -->
<div v-if="aardActions.length"
class="w100"
>
<div class="label">Enable autodetection {{scope === 'site' ? 'for this site' : ''}}:</div>
<div class="flex flex-row flex-wrap">
<ShortcutButton v-for="(action, index) of aardActions"
class="flex flex-grow button"
:class="{'setting-selected': getCurrent('autoar') === action.cmd[0].arg}"
:key="index"
:label="(action.scopes[scope] && action.scopes[scope].label) ? action.scopes[scope].label : action.label"
:shortcut="parseShortcut(action)"
@click.native="execAction(action)"
>
</ShortcutButton>
<div class="info"><small>Note: some sites implement restrictions that make autodetection a fair bit less reliable in Firefox and outright impossible in anything else.</small></div>
</div>
</div>
<!-- DEFAULT SETTINGS -->
<div v-if="stretchActions.length">
<div class="label">Default stretching mode:</div>
<div class="flex flex-row flex-wrap">
<ShortcutButton v-for="(action, index) of stretchActions"
class="flex b3 flex-grow button"
:class="{'setting-selected': getCurrent('stretch') === action.cmd[0].arg}"
:key="index"
:label="(action.scopes[scope] && action.scopes[scope].label) ? action.scopes[scope].label : action.label"
:shortcut="parseShortcut(action)"
@click.native="execAction(action)"
>
</ShortcutButton>
</div>
</div>
<div v-if="keyboardActions.length">
<div class="label experimental">Enable/disable keyboard shortcuts</div>
<div class="flex flex-row flex-wrap">
<ShortcutButton v-for="(action, index) of keyboardActions"
class="flex b3 flex-grow button"
:class="{'setting-selected': getCurrent('keyboardShortcutsEnabled') === action.cmd[0].arg}"
:key="index"
:label="(action.scopes[scope] && action.scopes[scope].label) ? action.scopes[scope].label : action.label"
:shortcut="parseShortcut(action)"
@click.native="execAction(action)"
>
</ShortcutButton>
</div>
</div>
<div v-if="alignmentActions.length">
<div class="label">Video alignment:</div>
<div class="flex flex-row flex-wrap">
<ShortcutButton v-for="(action, index) of alignmentActions"
class="flex b3 flex-grow button"
:class="{'setting-selected': getCurrent('videoAlignment') === action.cmd[0].arg}"
:key="index"
:label="(action.scopes[scope] && action.scopes[scope].label) ? action.scopes[scope].label : action.label"
:shortcut="parseShortcut(action)"
@click.native="execAction(action)"
>
</ShortcutButton>
</div>
</div>
<div v-if="otherActions.length">
<div class="label">Other actions:</div>
<div class="flex flex-row flex-wrap">
<ShortcutButton v-for="(action, index) of otherActions"
class="flex b3 flex-grow button"
:key="index"
:label="(action.scopes[scope] && action.scopes[scope].label) ? action.scopes[scope].label : action.label"
:shortcut="parseShortcut(action)"
@click.native="execAction(action)"
>
</ShortcutButton>
</div>
</div>
</div>
</template>
<script>
import ExecAction from '../js/ExecAction';
import KeyboardShortcutParser from '../../common/js/KeyboardShortcutParser';
import ShortcutButton from '../../common/components/ShortcutButton';
import ComputeActionsMixin from '../../common/mixins/ComputeActionsMixin';
export default {
data() {
return {
}
},
mixins: [
ComputeActionsMixin
],
props: {
settings: Object,
scope: String,
site: String,
},
created() {
this.exec = new ExecAction(this.settings, this.site);
},
components: {
ShortcutButton,
},
watch: {
settings: {
deep: true,
handler: function(val) {
this.$forceUpdate();
this.exec.setSettings(val);
}
},
site: function(val){
this.exec.setSite(val);
}
},
methods: {
execAction(action) {
this.exec.exec(action, this.scope);
},
getCurrent(option) {
if (!this.settings) {
return undefined;
}
let site;
if (this.scope === 'global') {
site = '@global'
this.site = site;
} else {
if (!this.site) {
return '';
}
site = this.site;
}
// console.log("SETTINGS FOR SITE", site, "option", option, JSON.parse(JSON.stringify(this.settings.active.sites)))
if (this.settings.active.sites[site]) {
return this.settings.active.sites[site][option];
} else {
return this.settings.getDefaultOption(option);
}
},
parseShortcut(action) {
if (! action.scopes[this.scope].shortcut) {
return '';
}
return KeyboardShortcutParser.parseShortcut(action.scopes[this.scope].shortcut[0]);
},
wipeSettings() {
settings.setDefaultSettings();
}
}
}
</script>
<style>
</style>

View File

@ -0,0 +1,5 @@
<template>
<div>
TODO: beggathon
</div>
</template>

View File

@ -0,0 +1,26 @@
<template>
<div>
PERFORMANCE PANEL :: This is here for debugging purposes.
<pre>{{performance}}</pre>
</div>
</template>
<script>
export default {
props: {
performance: Object,
},
watch: {
performance: {
deep: true,
}
}
}
</script>
<style>
</style>

View File

@ -0,0 +1,177 @@
<template>
<div class="w100" style="padding-bottom: 20px">
<div v-if="aspectRatioActions.length"
class="w100"
>
<div class="label">Cropping mode:</div>
<div class="flex flex-row flex-wrap">
<ShortcutButton v-for="(action, index) of aspectRatioActions"
class="flex b3 flex-grow button"
:key="index"
:label="(action.scopes.page && action.scopes.page.label) ? action.scopes.page.label : action.label"
:shortcut="parseShortcut(action)"
@click.native="execAction(action)"
>
</ShortcutButton>
</div>
</div>
<div v-if="true"
class="w100">
<div class="label experimental">Zooming and panning</div>
<div class="row w100"
>
<!--
min, max and value need to be implemented in js as this slider
should use logarithmic scale
-->
<input id="_input_zoom_slider"
class="w100"
type="range"
step="any"
min="-1"
max="4"
:value="logarithmicZoom"
@input="changeZoom($event.target.value)"
/>
<div style="overflow: auto" class="flex flex-row">
<div class="flex flex-grow medium-small x-pad-1em">
Zoom: {{(zoom * 100).toFixed()}}%
</div>
<div class="flex flex-nogrow flex-noshrink medium-small">
<a class="_zoom_reset x-pad-1em" @click="resetZoom()">reset</a>
</div>
</div>
<div class="m-t-0-33em w100 display-block">
<input id="_input_zoom_site_allow_pan"
type="checkbox"
/>
Pan with mouse
</div>
</div>
</div>
<div v-if="stretchActions.length">
<div class="label">Stretching mode:</div>
<div class="flex flex-row flex-wrap">
<ShortcutButton v-for="(action, index) of stretchActions"
class="flex b3 flex-grow button"
:key="index"
:label="(action.scopes.page && action.scopes.page.label) ? action.scopes.page.label : action.label"
:shortcut="parseShortcut(action)"
@click.native="execAction(action)"
>
</ShortcutButton>
</div>
</div>
<!-- <div v-if="keyboardActions.length">
<div class="label">Keyboard shortcuts:</div>
<div class="flex flex-row flex-wrap">
<ShortcutButton v-for="(action, index) of keyboardActions"
class="flex b3 button"
:key="index"
:label="(action.scopes.page && action.scopes.page.label) ? action.scopes.page.label : action.label"
:shortcut="parseShortcut(action)"
@click.native="execAction(action)"
>
</ShortcutButton>
</div>
</div> -->
<div v-if="alignmentActions.length">
<div class="label">Video alignment:</div>
<div class="flex flex-row flex-wrap">
<ShortcutButton v-for="(action, index) of alignmentActions"
class="flex b3 button"
:key="index"
:label="(action.scopes.page && action.scopes.page.label) ? action.scopes.page.label : action.label"
:shortcut="parseShortcut(action)"
@click.native="execAction(action)"
>
</ShortcutButton>
</div>
</div>
<div v-if="otherActions.length">
<div class="label">Other actions:</div>
<div class="flex flex-row flex-wrap">
<ShortcutButton v-for="(action, index) of otherActions"
class="flex b3 button"
:key="index"
:label="(action.scopes.page && action.scopes.page.label) ? action.scopes.page.label : action.label"
:shortcut="parseShortcut(action)"
@click.native="execAction(action)"
>
</ShortcutButton>
</div>
</div>
</div>
</template>
<script>
import ExecAction from '../js/ExecAction';
import KeyboardShortcutParser from '../../common/js/KeyboardShortcutParser';
import ShortcutButton from '../../common/components/ShortcutButton';
import ComputeActionsMixin from '../../common/mixins/ComputeActionsMixin';
export default {
data() {
return {
scope: 'page',
}
},
mixins: [
ComputeActionsMixin
],
props: [
'settings',
'frame',
'zoom'
],
created() {
this.exec = new ExecAction(this.settings);
},
components: {
ShortcutButton,
},
computed: {
logarithmicZoom: function(){
return Math.log2(this.zoom);
}
},
methods: {
execAction(action) {
this.exec.exec(action, 'page', this.frame);
},
parseShortcut(action) {
if (! action.scopes.page.shortcut) {
return '';
}
return KeyboardShortcutParser.parseShortcut(action.scopes.page.shortcut[0]);
},
resetZoom() {
this.zoom = 1;
},
changeZoom(nz) {
nz = Math.pow(2, nz);
this.$emit('zoom-change', nz);
this.exec.exec(
{cmd: [{action: 'set-zoom', arg: nz}]},
'page',
this.frame
);
}
}
}
</script>
<style>
.b3 {
width: 9rem;
padding-left: 0.33rem;
padding-right: 0.33rem;
}
</style>

18
src/popup/popup.html Normal file
View File

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="stylesheet" href="popup.css">
<% if (NODE_ENV === 'development') { %>
<!-- Load some resources only in development environment -->
<% } %>
</head>
<body style="width: 800px; height: 600px; overflow-x: hidden">
<div id="app">
</div>
<script src="popup.js"></script>
</body>
</html>

11
src/popup/popup.js Normal file
View File

@ -0,0 +1,11 @@
import Vue from 'vue'
import App from './App'
// global.browser = require('webextension-polyfill')
// Vue.prototype.$browser = global.browser
/* eslint-disable no-new */
new Vue({
el: '#app',
render: h => h(App)
})

16
src/res/css/colors.scss Normal file
View File

@ -0,0 +1,16 @@
$text-normal: #ddd;
$text-dim: #999;
$text-dark: #666;
$primary-color: #fb772a;
$secondary-color: #e70c0c;
$input-background: #141414;
$input-border: #4e3527;
$page-background: #101010;
$background-primary: #101010;
$selected-color: #f5cbaf;
$background-selected: #412d20;
$info-color: #bda9f3;

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