Compare commits

...

660 Commits

Author SHA1 Message Date
78395e3ea0 Minor fixes for comms and mutation detectìon 2026-06-06 03:29:46 +02:00
e269ff3d79 Don't trip on subtitles that don't fall outside current crop area
Todo: don't trip on subtitles that wouldn't get cropped, but that would require Aard to know player's aspect ratio, and it currently doesnt
2026-06-06 03:27:49 +02:00
be495b1c56 daon't error if context isnt defined 2026-04-03 16:44:41 +02:00
3f032619b4 Fix firefox errors in cross-iframe communiccation 2026-04-03 16:44:12 +02:00
ef5ba706b7 Changelog update 2026-01-29 00:00:11 +01:00
3b91c2224c Allow zoom modes to be set as default crop 2026-01-28 23:54:26 +01:00
822d069b82 Finish syncing UI with extension state
Add active mode indicators to popup. Add handling of locked zoom & alignment indicator to both popup and UI. Improve message passing through Comms.
2026-01-28 21:49:51 +01:00
8af9f2cedf Indicate currently selected option in the in-player popup menu 2026-01-28 18:11:57 +01:00
3d8d8eb199 broadcast scaling parameters on ar change 2026-01-28 02:44:38 +01:00
a0d599ce2f add amazon to extconfpatches 2026-01-28 02:05:11 +01:00
fd7b5ec04e Fix messaging 2026-01-28 02:02:27 +01:00
c9818c92b3 Update readme 2026-01-27 20:37:32 +01:00
f57163f2bb Player selection should be functional enough 2026-01-27 20:36:50 +01:00
dbdcb4e367 add option to always activate menu while holding CTRL 2026-01-25 23:33:37 +01:00
e1dd0f9c04 Open in-player settings window, get player selection to somewhat work 2026-01-25 21:47:31 +01:00
2cb6a88c5e fix UI settings a bit 2026-01-25 21:41:01 +01:00
eb89ba03db Fix config 2026-01-25 20:54:25 +01:00
cb02c8619d Ensure each command gets executed once, even if it gets duplicated on the way 2026-01-25 20:52:25 +01:00
7d296e6a3d Get in-page player picker to at least show up 2026-01-23 22:46:59 +01:00
35bb5b477b Finalize menu position settings, replace eventBus.sendToTunnel() with eventBus.send() 2026-01-15 02:33:21 +01:00
925a9fdea3 Other minor settings, add CSS we forgot about 2026-01-14 22:41:46 +01:00
f0baa6ad67 Enable distance-based UI showing. Add "show while holding CTRL" option to the in-player UI 2026-01-14 22:39:42 +01:00
f25749470b be a bit more dilligent about including visitedBusses 2026-01-14 22:38:08 +01:00
8a4985e6a3 Fix problem where aspect ratio wasn't getting set at all 2026-01-13 18:15:48 +01:00
2cce60fb9f Rework message passing and simplify tunnels 2026-01-12 01:07:45 +01:00
826acf9a3f add css for info and warnings 2026-01-12 01:06:23 +01:00
186de15781 fix manual player detection 2026-01-12 01:06:12 +01:00
17dc69f87a Fix player detection when top-layer is being used 2026-01-12 01:04:05 +01:00
8268c7a7af Housekeeping/convert some more imports to absolute paths 2026-01-07 18:04:34 +01:00
a71b99ff10 Mostly import fixes 2026-01-07 09:44:44 +01:00
54c4f49edb Sort out types 2026-01-07 01:42:37 +01:00
92cd1cf58b Fix enum parsing in site settings panel 2026-01-06 16:57:47 +01:00
64570c71ce Fix some default keyboard shortcuts + change some other behaviour about kbd 2026-01-06 01:10:14 +01:00
cbc4b7877f Add cross-iframe messaging (for UI windows) 2026-01-04 01:25:35 +01:00
b62f6feaea Open in-page settings inside floating windows 2026-01-04 00:54:59 +01:00
b6dec385dc Fix bug with zoom not working if AR hasn't been set yet 2026-01-03 23:30:45 +01:00
a12581ed0e Setup interactions for the custom elements of the in-player menu 2026-01-03 19:30:46 +01:00
e651e51c50 Add zoom sliders to in-player popup (non-functional) 2026-01-03 03:19:03 +01:00
56321e52fc Fix menu alignment, fix issue where overlay div sometimes incorrectly inherited bg color, allow custom HTML to be passed as string 2026-01-02 19:20:17 +01:00
73eec1f7ba Fix menu css 2026-01-02 17:47:42 +01:00
a0cddb256a WIP 2025-12-29 14:28:00 +01:00
14b3571af3 Get menu alignment to somewhat work 2025-12-29 11:32:49 +01:00
af08c5094d Fix CSS processing 2025-12-29 02:34:46 +01:00
ef12dc8cc5 Start working on client menu, sort out CSS injection 2025-12-29 00:21:03 +01:00
fcfc0361dc Settings page is now on some baseline of "works", but needs some minor touch-ups 2025-12-28 00:13:06 +01:00
a8847bdeff Save player element settigs + simplified import/export 2025-12-27 23:22:54 +01:00
3f5ab5ecc6 WIP on player detection config import 2025-12-27 03:20:28 +01:00
077c7cae15 Fix migrations of extension enabled status 2025-12-26 22:50:56 +01:00
f916cffd41 Fix migrations 2025-12-26 22:32:36 +01:00
ea4fb95a3c Version bump 2025-12-26 21:40:19 +01:00
3dbec85269 Migrate DOMConfig in preparation for new settings page 2025-12-26 21:39:34 +01:00
2a42c3e715 More or less finish rework of how setting defaults work 2025-12-26 21:36:15 +01:00
c2a6a3f082 Extension enabled mode is now being set correctly 2025-12-22 14:48:50 +01:00
61972a7b2d Fix logger init in popup 2025-12-22 03:53:20 +01:00
4a562443b1 Fix CSS styles not getting applied 2025-12-22 03:36:11 +01:00
9515c1b01a Various fixes, trying to get application of user css styles to work again- 2025-12-22 03:23:10 +01:00
126d891e5e Work around chrome's console.log limitations. 2025-12-22 02:55:33 +01:00
dad1df0244 FIX LOGGER 2025-12-20 18:06:11 +01:00
aff429e5ea Start working on advanced player selector 2025-12-18 00:10:55 +01:00
3457c4fcdc cleanup 2025-12-17 18:35:24 +01:00
74c0358d33 Add snapshot creation and snapshot editing _for real_ 2025-12-17 01:50:50 +01:00
8735d3e81a Removed previous @updated page 2025-12-16 22:58:54 +01:00
4180d5b5b1 Settings snapshot handling, moved update page to unified settings page 2025-12-16 22:57:19 +01:00
ca751a660f Move files around, clean up imports 2025-12-15 19:42:07 +01:00
fb7f5cf1ba More migrating 2025-12-11 02:43:39 +01:00
fe2f52d76a Bump version 2025-12-10 22:50:56 +01:00
ee32ae93c3 Simplify extensionMode things 2025-12-10 22:49:59 +01:00
877fc96da1 Add LegacyExtensionMode enum 2025-12-10 22:11:23 +01:00
67a5b4e05a update nvmrc 2025-12-10 22:11:01 +01:00
d0a5083841 WIP popup redesign 2025-12-10 22:10:40 +01:00
2d8c078a6f Ensure SVG styles don't escape containment 2025-12-03 03:23:31 +01:00
f543dcff24 Start on work on popup, mostly finish work on settings page 2025-12-03 03:15:43 +01:00
5cfa5b0add Redesign of autodetection options 2025-12-02 01:03:54 +01:00
13feb1e822 Merge branch 'dependency-upgrades' 2025-12-02 01:03:05 +01:00
dc755dbe62 Finish migration to tailwind, add new settings screen 2025-12-02 01:02:09 +01:00
b334707463 Convince tailwind to work 2025-12-01 01:14:08 +01:00
ca093f5b34 Upgrade libraries 2025-12-01 01:03:12 +01:00
2e4226b828 cleanup of player detection 2025-11-30 21:27:26 +01:00
095a3daadd Refactor playerData/player size changing a bit. Settings can now only be patched from ExtConfPatches 2025-10-14 20:26:35 +02:00
baa7c79f6c this should be included in "re-add legacy aard" commit 2025-10-14 18:56:48 +02:00
f5e308c5b4 Add update screen 2025-10-14 18:55:45 +02:00
c0c29759fa Re-add legacy AR detect 2025-10-14 18:54:06 +02:00
ddffbe92a0 Finish rewriting Aard 2025-10-13 17:35:37 +02:00
888e48eb5b Refactor aard 2025-10-12 22:03:55 +02:00
61e7351d0b Version bump 2025-10-08 19:40:49 +02:00
4b0571e123 Change how small player windows are checked 2025-10-08 19:40:28 +02:00
bf7f707ab7 change some bits 2025-10-08 19:39:55 +02:00
accc320971 Add options for controlling how aard runs in small players and background tabs + allow auto-disabling aard after first detection 2025-10-08 19:37:25 +02:00
4276a9f03a More logging while building 2025-10-08 16:54:59 +02:00
7ae3fb6109 Fix angle backend detection, though that should no longer be relevant 2025-10-08 15:06:19 +02:00
8c96e2fd90 New iframe inheritance should somewhat work now 2025-08-03 02:58:59 +02:00
30e5bce38f Changelog fix 2025-08-03 02:58:55 +02:00
620028db6d Add validation to aspect ratio input in shortcut settings, fix parsing of aspect ratios given as X:Y 2025-08-03 02:58:02 +02:00
850b5d6b25 Add 'iframes shouldn't inherit' option to site settings 2025-07-16 22:27:05 +02:00
eaf003f5a7 Rework player selector tutorial 2025-07-16 22:26:25 +02:00
ab1efbe9cf Provisional version bump 2025-07-16 03:17:14 +02:00
14f8453067 Update player UI element on fullscreen change; when in fullscreen, limit potential player UI elements to full screen element (if it exists) 2025-07-16 02:53:40 +02:00
090f112463 Get iframe inheritance to work in the popup 2025-07-16 02:49:19 +02:00
01d569f1b5 Update missing types 2025-07-15 19:46:22 +02:00
75670ef3f9 Move settings to a different folder, iframes default to parent page settings 2025-07-15 19:42:35 +02:00
f88f703e84 move settings (update imports only) 2025-07-15 19:41:00 +02:00
a5fdef0196 version bump for release 2025-06-18 02:19:18 +02:00
f10eee335d version bump for testing version 2025-06-18 01:41:02 +02:00
20856891b6 Don't open global settings window in iframe 2025-06-18 01:15:45 +02:00
60dd1193ab Detect embedded content 2025-06-18 01:04:42 +02:00
2bc95af73c Fix iframe detection 2025-06-17 22:49:16 +02:00
178bb65b4a Improve embedded frame detection 2025-06-17 20:12:44 +02:00
f7f046ea53 Propagate messages across server boundary 2025-06-16 21:58:35 +02:00
ec0896e17a Implement zoom in extension popup, in-player UI 2025-05-19 01:18:48 +02:00
a5f35248bd Event-bus: Auto-forward across iframe and commsServer 2025-05-19 01:17:09 +02:00
a05eccce9e fix zoom config 2025-05-19 01:15:05 +02:00
e118777636 Fix debounce 2025-05-19 01:14:19 +02:00
7998e8158e Fix log coloration 2025-05-07 00:45:08 +02:00
14f837d1f0 EventBus: prevent backflow of messages that originate from CommsServer back into CommsServer 2025-05-07 00:42:25 +02:00
cff1d3cf16 Cache active tab in comms server
Previously, if console window was active, this.activeTab would return nothing, and the popup would break. If that happens now, popup will pretend it's on the last active site.
2025-05-07 00:41:45 +02:00
b2cd96982e Further logging fixes 2025-05-04 21:00:15 +02:00
a89eb8e857 Whoops, missed some bits in the cleanup 2025-05-04 02:37:45 +02:00
7c1ccd2cc9 Add contributing.md and license.md 2025-05-04 02:30:56 +02:00
e3a12bc601 Cleanup of unused imports 2025-05-04 02:29:33 +02:00
e20508956a Replace old logger with new logger 2025-05-04 02:18:58 +02:00
4000d0e027 Start rewriting logs 2025-05-01 00:55:22 +02:00
77fcced776 Fix saving settings for sites that inherit configurations from a different site 2025-04-28 01:38:13 +02:00
e67b5a227f Allow some cross-domain settings sharing, handle settings inheritance, don't save auto-detected player index 2025-04-28 01:23:12 +02:00
150a0a8a90 Process iframe data in the popup, remove vestigal code 2025-04-27 19:51:21 +02:00
269dddc92e Fix legacy canvas loading 2025-04-27 17:53:50 +02:00
508ef5cbbb Fix calculation of averages in AardTimers 2025-04-27 17:52:04 +02:00
500d06204c Move default settings to 'other sites' tab 2025-04-27 01:12:06 +02:00
fe8436179f Remove orphan components, update dictionary 2025-04-26 23:28:17 +02:00
adedfec1d0 Update popup appearance 2025-04-26 23:27:09 +02:00
b974111eb4 Fix zoom 2025-04-26 23:24:57 +02:00
3d7b50a2e3 Get zoom options to mostly work 2025-04-26 04:23:57 +02:00
32c11d1f61 Fix snapshots 2025-04-26 01:11:51 +02:00
5c48747d0f Add new zoom options (but they don't do anything yet) 2025-04-26 01:11:31 +02:00
5a0abb3595 Fix settings migrations 2025-04-26 01:09:49 +02:00
95b98c9053 Fix loading snapshots 2025-04-26 01:08:43 +02:00
518e09e3cb Changelog 2025-04-22 02:53:05 +02:00
ad632b0680 Add snapshots to settings page 2025-04-22 02:52:41 +02:00
73aa925067 Don't do AR changes on negative aspect ratios 2025-04-22 02:44:28 +02:00
391b0ac7ab Settings snapshots 2025-04-22 02:43:44 +02:00
ae2bb7afcd Fix UI hiding 2025-04-22 02:37:36 +02:00
0681c3ae08 UI improvements 2025-04-22 02:36:34 +02:00
4aa96bb288 Changes to debugging UI 2025-04-21 22:24:32 +02:00
b24739d70d Put "pls report bugs" in 'what's new' panel as well 2025-04-20 19:50:51 +02:00
dffe8e055a put dev overlays into settings 2025-04-20 19:29:47 +02:00
adadc8bb3e version bump 2025-04-20 19:28:59 +02:00
5f3562fe5b Remove orphan settings 2025-04-20 16:11:57 +02:00
0d7c535a70 revive zoom 2025-04-20 16:11:17 +02:00
39f39f23a7 Prevent more than one concurrent trackDimensionChanges from running 2025-04-15 19:23:42 +02:00
ff0de6f2e7 Version bump 2025-04-15 19:05:48 +02:00
8658b3c0d3 Changelog 2025-04-15 19:03:29 +02:00
8551bc2dc1 Add performance measurements to Aard 2025-04-15 18:51:34 +02:00
a0abf336cf fix #269 2025-04-10 00:46:44 +02:00
7d71dd0507 Allow debug canvas to be shown or hidden from settings 2025-04-10 00:46:22 +02:00
54c190f86c Updated toys 2025-04-05 01:31:06 +02:00
e198c69741 Added toys that enable visual debugging of Aard 2025-04-03 02:59:25 +02:00
7c1d3e52e5 Instead of elements closer to the <video> tag, prefer best-matching element closer to page root.
This ensures UI always renders on top of player if enabled.
2025-03-31 23:26:03 +02:00
5460719297 css fixes 2025-03-31 00:28:35 +02:00
83b770e4e8 Start updating changelog 2025-03-31 00:26:14 +02:00
a5c9835d9d Fix problem where 'default' is always 'disabled' 2025-03-31 00:20:43 +02:00
f0840b78e1 Change handling of getPlayer() 2025-03-30 23:49:13 +02:00
ab79f1e0e6 TODO: some day finish rewriting UI in a way that loads iframe only when expressly requested. 2025-03-30 23:48:24 +02:00
16a2f511bb Replaced v-model in <select> with :value
Did this really fix #270?
2025-03-30 01:27:28 +01:00
39fdd72e9f Don't propagate bad site data from uwserver to extension popup 2025-03-30 01:24:37 +01:00
f93a7e284c Remove InPlayerUIAdvertisement component 2025-03-30 01:24:08 +01:00
b5e480a486 'dev mode' is now saved under settings. 'debug' panel is only shown if dev mode is active 2025-03-30 00:54:47 +01:00
bf128babcc Started logger rewrite. Gave up. 2025-03-30 00:27:50 +01:00
46d1ad221f Re-add debugger panel, strip down json editor 2025-03-30 00:04:30 +01:00
7aacbec108 Reset settings editor on settings import and reset 2025-03-29 17:17:20 +01:00
f77506c454 Settings export, settings import, json editor for settings 2025-03-29 17:11:04 +01:00
e3f1f21ce9 Add basic confirm button. 2025-03-18 18:58:31 +01:00
08c86615a0 Move 'show in-player UI' to popup header 2025-03-14 02:46:59 +01:00
b0425b37c0 minor style tweaks 2025-01-30 02:40:04 +01:00
d94be7aa5e Changelog.MD update 2025-01-30 01:31:11 +01:00
40bdddd031 Remove unnecessary things from the overlay 2025-01-30 01:30:08 +01:00
da94dee780 Make window background darker until chrome doesn't fix backdrop-filter bug 2025-01-30 01:29:48 +01:00
587a21c5f6 Fix default settings 2025-01-30 01:29:00 +01:00
f994617d44 Don't reset settings on failed extconfpatches application 2025-01-30 01:28:44 +01:00
a92f5fc2a1 Fix enabling/disabling extension while switching between environments 2025-01-29 23:58:16 +01:00
208a360c47 Fix settings cascade in ui 2025-01-29 23:56:50 +01:00
f728cbf817 change styles for scrollbars 2025-01-28 00:48:20 +01:00
aef5665cb5 Don't incorrectly set websites as user-defined just because initial player index was set 2025-01-28 00:47:45 +01:00
6ea47d26d0 remove cta from UI advertisement 2025-01-28 00:42:56 +01:00
a7a36033a2 Version bump 2025-01-27 02:59:54 +01:00
32f4d99c98 Move player UI settings and make them site-dependent. Fix SiteSettings always overriding ExtensionStatus.Default with actual default value 2025-01-27 02:59:30 +01:00
2d08a2f339 Hide 'set default stretch' in stretch panel 2025-01-27 02:41:41 +01:00
1f0ebff241 Fix UI not reacting to settings updates 2025-01-26 23:57:02 +01:00
5c26e49c17 disable settings if extension is disabled 2025-01-26 22:10:30 +01:00
ea85f2f5d6 Disable extension on unknown sites by default 2025-01-26 22:10:06 +01:00
af9b123e8d Rework settings popup 2025-01-26 22:09:32 +01:00
d322c0909a Merge branch 'hotfix/iframe-fix' 2025-01-25 21:19:55 +01:00
0bfe84d73d fix saving settings 2025-01-25 21:18:04 +01:00
244e8227e7 Settings: make setProp() functional 2025-01-25 21:17:20 +01:00
5da8073235 fix conflict 2025-01-25 21:15:26 +01:00
5479b7ee95 Fix the iframe problem 2025-01-25 20:51:22 +01:00
b4e932581c fix setting default alignment 2025-01-14 02:41:20 +01:00
1f65cca002 fix support level indicator 2025-01-14 02:41:06 +01:00
1a9cdfa93b additional fixes 2025-01-14 01:51:16 +01:00
d780a8cb12 Fix stretch mode 2025-01-14 01:44:25 +01:00
eaf619f4f8 Revert "Temporarily comment out zoom from player overlay"
This reverts commit ebdc26a00d.
2025-01-13 01:42:58 +01:00
3dd8584954 disable zoom options in player UI for the time being 2025-01-13 01:42:38 +01:00
ebdc26a00d Temporarily comment out zoom from player overlay 2025-01-13 01:33:58 +01:00
dd47dd09da Add about panel back to the popup 2025-01-13 01:31:59 +01:00
bc6d2ac549 Add call to arms to popup. Fix some minor issues 2025-01-13 01:22:49 +01:00
b2cd8197d4 Updated labels of in-player UI 2025-01-12 23:25:39 +01:00
23ea9e77c4 Added zoom UI 2025-01-12 23:25:20 +01:00
42ac51f1e9 CSUI: set opacity to 0 when extension menu is not triggered 2025-01-12 23:24:01 +01:00
d433878680 stop using relative imports in some non-css components 2025-01-12 23:21:51 +01:00
11fa12001c stop using relative imports for most of css 2025-01-10 19:13:27 +01:00
a82a6963e9 version bump 2025-01-10 19:12:40 +01:00
a98a80001c basic iframe for notifications 2025-01-07 08:23:11 +01:00
ad1e73d0cc Try to detect when UI is not clickable 2025-01-06 03:05:18 +01:00
55c0e7e8c5 Get DRM status to UI more reliably 2025-01-05 02:47:45 +01:00
3d35ae32c8 Throw error if upgrading settings fails 2025-01-05 00:38:11 +01:00
33e3bec337 Fix stretch mode while not cropped 2025-01-05 00:37:20 +01:00
9c12272846 attept detecting when wrong player element is detected pt 1 2025-01-05 00:36:41 +01:00
0cc2249b97 version bump but in manifest file 2025-01-05 00:08:09 +01:00
a5d0c0b6b7 fix support level indicator 2025-01-05 00:06:59 +01:00
3736a55bad version bump 2025-01-05 00:06:43 +01:00
303213da73 fix netflix fix some more 2025-01-02 06:32:43 +01:00
8d47eaf74c fix color-scheme issue 2025-01-02 05:50:19 +01:00
9f10c0dcf4 FOR REALS account for offset applied by sites that use top:50%/transformY:-50% to center elements 2025-01-02 04:33:00 +01:00
d981e4c2c6 Account for offset applied by sites that use top:50%/transformY:-50% to center elements 2025-01-01 22:18:37 +01:00
8f3d1bd091 Add illustrations to the player picker 2025-01-01 22:15:22 +01:00
3b9f557075 version bump 2025-01-01 22:14:10 +01:00
18cdec51d9 settings migration 2024-12-31 03:36:08 +01:00
276a51b8c3 WIP on master 2024-12-31 03:15:10 +01:00
14e1d6aad3 even more aard updates 2024-12-31 03:14:29 +01:00
9c65300fc4 more aard updates 2024-12-31 02:50:33 +01:00
cf01dd9397 Try to reduce message-passing to improve performance, make ultrawidify not run on videos smaller than 720p 2024-12-30 23:02:55 +01:00
162318b439 Remove legacy code 2024-12-30 21:39:50 +01:00
cd89cca984 fix vertical alignment 2024-12-30 03:05:47 +01:00
25222c7310 Prevent overlay from blocking the video player UI 2024-12-30 00:55:58 +01:00
b2bfde4426 Fix sliders, but for real this time (pt 2) 2024-12-28 03:41:23 +01:00
8602b53260 Fix sliders, but for real 2024-12-28 03:14:53 +01:00
145b361581 Fix 2024-12-28 02:05:42 +01:00
7743dd8e7f Add sliders 2024-12-28 02:02:01 +01:00
ed2886d449 add minEnabledHeight to settings 2024-12-28 01:42:13 +01:00
002dfacc3e change timeout a bit 2024-12-28 00:42:53 +01:00
fd9fbc32a8 Remove some console.logs 2024-12-28 00:42:40 +01:00
e54deeb4f7 save settings when changing parameters 2024-12-28 00:41:07 +01:00
c85f211d5e Add reset settings to options 2024-12-27 23:13:12 +01:00
67f848a9ee Unbreak showing popup on hover 2024-12-27 23:12:41 +01:00
fe336122e0 Make crop iptions and stretch options panels editable by default without breaking the popup. Add them to UI and keyboard section of settings 2024-12-27 13:18:31 +01:00
6d8d8493a6 Zone editor mostly finished 2024-12-27 04:41:35 +01:00
5d7d249fff Fix overlay positioning 2024-12-27 04:40:30 +01:00
35ca030dfb Close window when opening zone edotir 2024-12-26 17:54:05 +01:00
fab6fb0f5b Fix mousemove position calculation 2024-12-26 17:41:00 +01:00
3b8fc807f7 Add corner markers to editor resizer area 2024-12-26 17:40:27 +01:00
13cfb9ff14 Checkpoint: aard status indicator + trigger zone editor now kinda works 2024-12-26 14:58:14 +01:00
aabd5e75d8 Update changelog 2024-12-22 23:29:43 +01:00
7be6fb9501 Add fallback option class (currently unused) 2024-12-22 23:20:07 +01:00
9fc63e5cd3 Add player UI settings panel 2024-12-22 23:17:32 +01:00
7a1e8be48a attempt to shut some warnings up (but fail) 2024-12-22 23:16:54 +01:00
d299a65e03 fix — open on correct tab 2024-12-22 23:07:55 +01:00
2337f7a3ba add new options to settings interface 2024-12-22 23:06:14 +01:00
4853d7e2fd allow opening of extension settings window on specific tab 2024-12-22 23:05:56 +01:00
ca1a375f2d Limit when in-player UI shows. Add self-expiring menu options that takes you to the proposed, not-yet-implemented UI settings screen 2024-12-17 02:02:49 +01:00
0d098815a2 fix aspect ratio calculations 2024-11-13 01:46:17 +01:00
30cbfb6ffd Make sure that guard line gets invalidated correctly 2024-11-13 01:45:45 +01:00
4c7deffdfe Do full reset of AARD test result every time aard restarts 2024-11-13 01:43:07 +01:00
15133e561f Get autodetection to work at least on a basic level 2024-11-07 02:26:19 +01:00
c235b52e3b fix canvas initialization, add add methods to attach canvas to main body for ez debug 2024-11-07 00:18:40 +01:00
903f356252 Make the context menu slightly less rudimentary 2024-10-24 01:47:46 +02:00
54c44b4379 Add very rudimentary context menu 2024-10-23 01:23:54 +02:00
dcbeecc8cf fix syntax 2024-10-23 01:23:37 +02:00
bf182c65ad Aard should be nominally complete. Probably. 2024-10-22 23:43:25 +02:00
7c5e4101b0 Finish aspect ratio check 2024-10-21 01:08:03 +02:00
2736ac418f WIP on aspect ratio detection 2024-10-19 16:04:20 +02:00
e2dac10501 Finish guardline/image line tests 2024-10-15 17:38:04 +02:00
c15d7ab28e Start re-implementing aspect ratio detection (now with more documentation) 2024-10-14 00:28:13 +02:00
de78f8b1d5 Switch to gl canvas 2024-10-10 00:45:28 +02:00
e56d32180c Add GL canvas wrapper 2024-10-09 23:54:21 +02:00
6c92837317 wip 2024-06-12 20:29:00 +02:00
d4015a86ef Version bump 2024-06-12 09:17:52 +02:00
4b4be96e26 Fix one more link 2024-06-12 09:09:25 +02:00
823e1d983c Remove currently inactive downloads permission 2024-06-12 09:06:12 +02:00
47fba5b81a Fix external links 2024-06-12 03:15:02 +02:00
ee23ebb705 Update permissions for mmanifest v3 2024-06-12 02:17:03 +02:00
d6d3a95daf Last pre-publish fixes 2024-06-12 01:55:12 +02:00
e839e632fc Merge branch 'feature/player-ui' 2024-06-11 02:39:09 +02:00
cb6f0a9a32 Changelog.MD update 2024-06-11 02:36:36 +02:00
c5515934d9 Do the changelog + highlight changelog tab 2024-06-11 02:22:53 +02:00
7dafe85c53 Do the version bump 2024-06-11 01:59:11 +02:00
3bc65c252d I think i disasbled size requirements for in-player UI. Changed wording in popup to reflect that 2024-06-11 01:56:21 +02:00
d4f9ab9f37 Fix player detector panel 2024-06-11 01:55:02 +02:00
b96227b07a plan ahead 2024-06-10 23:40:49 +02:00
da12ee452c Get aard to start reliably if it's set as default aspect ratio for the site 2024-06-10 23:40:33 +02:00
99df043b5f Fix Aard pausing/unpausing when not intended 2024-06-10 23:27:47 +02:00
54d53f1b91 Remove unused code to make Aard work 2024-06-10 23:27:00 +02:00
3645b9ffcf fix typo 2024-06-10 23:26:08 +02:00
9d0ce25be7 Ensure videos get aligned even when aspect ratio is not defined 2024-06-10 00:08:49 +02:00
1c860f74fb remove wiki link, as it's innacurrate 2024-06-09 21:28:57 +02:00
75e31dc3a4 Improve alignment ui 2024-06-09 21:25:11 +02:00
10a8452709 Include changelog 2024-06-09 21:24:26 +02:00
f824f73491 update some libraries 2024-06-09 21:23:25 +02:00
16c34e3d52 ui styling 2024-06-08 03:23:49 +02:00
76c4599dee prepare class names for a day in the future, when we'll move to tailwind 2024-06-08 03:23:34 +02:00
1af80096c7 Add changelog, about panel 2024-06-08 02:10:50 +02:00
cbb2bb4502 Shortcut editor alert 2024-06-06 00:51:47 +02:00
0f3bcaa8e6 Fix alignment/offset handling, make it as player-invariant as possible 2024-06-06 00:51:34 +02:00
ae6575043e fix keyboard shortcut handling for default shortcuts 2024-06-06 00:50:58 +02:00
0dbd5b456c Fix setting keyboard shortcuts 2024-06-06 00:50:08 +02:00
81f3e62446 Pin and close buttons 2024-06-05 23:52:50 +02:00
e0bb25466d UI/global UI mostly works 2024-06-05 01:08:50 +02:00
cb334aab0d Do a bad code, because chrome doesn't work when you try to write good code 2024-06-03 00:16:40 +02:00
9cef850d06 Make extension work on Chrome. Make extension NOT work on firefox. 2024-06-03 00:16:12 +02:00
79ecc87db4 WIP trying to convince in-player UI to show 2024-06-02 16:06:26 +02:00
12644bfbea get manifest v3 to approximately work 2024-05-30 01:02:09 +02:00
bc6b58aa3f Ensure transform offsets are is rounded 2024-05-07 21:02:22 +02:00
abeded1d03 Further migrations to runlevel 2024-05-07 21:02:04 +02:00
a466f77b6e Merge branch 'master' into feature/player-ui 2024-05-07 20:28:39 +02:00
968ce5432a RunLevel works, even though half-assed 2024-05-07 20:24:50 +02:00
277506b3c8 Start implementing run levels 2024-05-07 19:05:10 +02:00
1c48ea1908 Reorder panels in video settings 2024-05-04 01:08:42 +02:00
f2ac1557a4 Fix icon & heading sizes in popup 2024-05-04 01:08:32 +02:00
7213c1e35f Fix padding 2024-05-04 01:08:25 +02:00
9f5f953b46 'Edit shortcuts' bar banner now doesn't take up vertical space while inactive 2024-05-04 01:08:21 +02:00
3ff6054c20 Fix popup (css now finally workS!) 2024-05-04 01:08:14 +02:00
9fb25254a6 Import CSS in vue component instead of html, otherwise popup is without css 2024-05-03 23:13:55 +02:00
8c6a06d490 move fonts 2024-05-03 23:13:07 +02:00
dd37837848 Update node 2024-04-07 15:37:16 +02:00
5ca9325e16 fix 2024-01-28 13:08:31 +01:00
b8fdf0911a Update changelog 2024-01-28 13:01:45 +01:00
a7302fee4b fix angle detection 2024-01-28 13:01:16 +01:00
41bf4d2764 fix 2024-01-28 13:00:48 +01:00
0970140a3a Version bump 2024-01-27 23:29:03 +01:00
b8eb8ec052 rework about panel 2024-01-27 23:06:56 +01:00
46b7d6a25a Round pixel offset 2024-01-27 22:24:09 +01:00
aab2f5b014 fix ard performance display 2023-10-21 17:59:52 +02:00
6062094e6f Fix crashes in chrome 2023-10-21 17:41:09 +02:00
f7e36619df remove manifestv2 2023-10-21 14:04:02 +02:00
167f5173cd get popup to at least mildly work 2023-09-11 00:43:26 +02:00
c7ebd42fe9 some more moving around 2023-09-10 22:09:26 +02:00
94018c6ee3 Cleanup file/directory structure for the UI 2023-09-10 22:06:16 +02:00
388327ec1d fix fs handlers 2023-09-10 19:51:36 +02:00
0ba56c271d hardcode defautl css string somewhere else 2023-09-10 13:12:35 +02:00
bd1216c275 get player select UI to work 2023-07-15 04:17:38 +02:00
3180063fe3 attempt to get player selector to work 2023-07-15 04:03:32 +02:00
9cb9a6607b Remove unused components 2023-07-15 01:29:57 +02:00
42cdcd5c25 Site and extension options: done 2023-07-15 01:28:20 +02:00
8b1da27af7 remove UI when extension is disabled 2023-07-11 00:48:34 +02:00
638e228ce9 don't init UI by default 2023-07-10 22:56:26 +02:00
94469ccd0e mess around with launch.json, still didnt get debugger to work, decided i dont care 2023-07-10 22:05:37 +02:00
48f7e1f39c Merge branch 'feature/manifest-v3-v2' into feature/player-ui 2023-07-10 22:05:02 +02:00
60f7abfa42 get manifest v3 to approximately work 2023-07-10 22:04:30 +02:00
cd391db302 wip 2023-07-10 18:27:06 +02:00
8613d6971f Showing aard notifications is no longer implemented, so we don't call it 2023-04-16 02:54:30 +02:00
cc18153889 Minor update to site settings 2023-04-16 02:53:59 +02:00
be4bf9e2a3 detect whether extension is enabled on dimension changes. Trigger dimension check on fullscreen change 2023-04-16 02:43:50 +02:00
085bd4aab9 fix video alignment default settings 2023-04-16 02:16:57 +02:00
977d103c3a remove aard::halt() (is duplicate of aard::stop() anyway) 2023-04-16 01:55:11 +02:00
0778f07922 sensible variable naming is important + remove redundant code 2023-04-12 23:08:19 +02:00
e12154601b also add 32:9 button 2023-03-30 00:49:09 +02:00
b22d50f760 Firefox also stopped liking the -alpha# format, so version numbering was switched from [next major]-X.X-[alpha|beta|dev]# to [current-major].99.X-# 2023-03-30 00:45:34 +02:00
302f21b477 Cycle mode 2023-03-30 00:43:30 +02:00
65da515854 various fixes 2023-03-29 22:07:50 +02:00
e9bd3154a5 change event bus parameter names 2023-03-29 21:51:06 +02:00
1da147e110 remove redundant comments 2023-03-29 21:49:27 +02:00
e7dd85420c iframe manager 2023-03-02 01:08:25 +01:00
3e542871c6 Attach source frame info when communicating between frames on the same page 2023-03-02 01:08:16 +01:00
26b78f1225 EventBus: Allow forwarding between iframes of a page 2023-03-02 00:37:23 +01:00
57261b5094 Extension settings now holds both extension and site settings 2023-01-23 00:15:56 +01:00
c7f24133e5 fix ANGLE 2023-01-23 00:15:21 +01:00
87aa397a7c Version bump + changelog 2023-01-22 18:04:26 +01:00
e2a18ec765 Extconfpatch for disney+ 2023-01-22 17:58:39 +01:00
26ee2ffbb6 ANGLE popup 2023-01-22 17:45:33 +01:00
38dd1ddbe8 ANGLE detection 2023-01-22 16:06:33 +01:00
11bc79520a ANGLE detection 2023-01-22 16:05:07 +01:00
cc0ae60c83 Update settings object, finalize-ish site settings panel 2023-01-15 22:11:47 +01:00
30b028d836 remove redundant code 2023-01-15 22:10:17 +01:00
a30a70c6b5 get UI to somewhat work. TODO: get scripts to work 2023-01-07 19:19:44 +01:00
9503003a4a migration to site settings mostly done. TODO: check if things still work 2023-01-07 18:57:47 +01:00
a58edad8ea migrate settings to new and simplified SiteSettings 2023-01-07 03:06:37 +01:00
d0ba619b45 Redefine settings interface, write function to migrate some settings on update 2023-01-07 03:05:51 +01:00
64bad6758b start work on new panels 2023-01-07 03:05:03 +01:00
b902b8f9ba fix some warnings 2023-01-06 19:03:44 +01:00
7bb4ee96e7 return zoom slider to its rightful place 2023-01-06 19:03:35 +01:00
6c3e960096 Player is in a separate component 2023-01-06 18:47:42 +01:00
cacb0f1ea0 Put player window into its own component 2023-01-06 18:23:34 +01:00
5a2e33ccd2 fix 2023-01-06 18:23:12 +01:00
5d389a0796 Rename a file for future consistency 2023-01-06 18:22:15 +01:00
6db1d8af2a remove old dist before build 2023-01-06 18:19:59 +01:00
51d73e09ae ?? 🤔🤔🤔🤔 2023-01-06 18:17:08 +01:00
3c324aa1b2 Add global page (not loaded by extension yet), put mousemove probes in a separate file 2023-01-04 18:09:01 +01:00
a94092ac78 remove unnecessary components 2023-01-04 18:08:11 +01:00
f9d72d12b6 minor changes 2023-01-04 18:07:55 +01:00
81e11c2ae9 Extension options WIP 2022-11-22 01:28:04 +01:00
f7a82ccdf7 re-add missing method 2022-11-21 20:46:51 +01:00
44e3395835 Get basic messaging from popup to CS working. TODO: Forwarding to specific iframes/videos 2022-11-21 01:04:57 +01:00
d558717152 Try to recreate some UI in popup 2022-11-20 23:41:40 +01:00
b9b021f466 Spin off VideoSettings component into subcomponents 2022-11-20 22:45:34 +01:00
13bfd63dc2 Move common things between mousehandler and kbhandler to kbmBase 2022-11-20 17:26:57 +01:00
9600c4f1c9 fix kbm whoopsie 2022-11-20 15:23:23 +01:00
de9696471a kbm refactoring stuffs 2022-11-20 14:46:01 +01:00
c954f8b984 add playerMouseHandler 2022-09-28 01:18:58 +02:00
08479242f7 fix settings 2022-09-28 00:40:09 +02:00
d70783cc82 Rename canStartExtension to more descriptive isEnabledForSite 2022-09-28 00:39:20 +02:00
a2f0617d5a start splitting kbmHandler into keyboard and mouse handler 2022-09-28 00:38:36 +02:00
27b6ca824d fix action handler rename 2022-09-27 22:15:01 +02:00
e84f8ee541 start fixing KbmHandler (formerly: action handler) some more 2022-09-27 01:48:08 +02:00
ad87dbdcb9 remove unnecessary logging 2022-09-20 01:38:08 +02:00
9c42c120f5 Remove unnecessary components 2022-09-20 01:35:47 +02:00
740f2e8f6f fix forwarding data between server, content script, and popup 2022-09-20 01:34:59 +02:00
a0388689cf Continue slowly working on unified event bus 2022-08-21 22:46:06 +02:00
46cb5e1078 npm fix 2022-08-21 15:50:03 +02:00
abc53cf00f don't fuck ArDetector up 2022-08-21 15:49:28 +02:00
68f367bd39 Merge branch 'master' into feature/player-ui 2022-08-21 15:45:11 +02:00
d54097799c update cleanup script 2022-08-21 15:24:34 +02:00
ffc25520bb fix types 2022-08-21 15:24:24 +02:00
ccdc68870a Fix #199 2022-08-21 15:04:21 +02:00
7820da181e Readability fixes (I guess) 2022-08-20 16:04:31 +02:00
ec4d43d2ec unpause: Don't resume autodetection if not paused 2022-08-08 23:17:13 +02:00
c12a04b55f mostly whitespace 2022-08-08 23:16:55 +02:00
e0fe4c7103 Version bump + changelog 2022-08-06 01:56:12 +02:00
82f2983ac2 package.lock update 2022-08-06 01:31:40 +02:00
d2733cf486 Fix autodetection getting stuck in 21:9 2022-08-06 01:31:18 +02:00
3aabd298ec fix various fuckies wuckies 2022-07-31 01:12:54 +02:00
a0e375d2dc Cleanup — remove old uwui implementation 2022-07-31 00:18:21 +02:00
8806c8ea0c Integrate comms client/server into eventBus 2022-07-31 00:15:28 +02:00
f0120010fe do types 2022-07-31 00:14:40 +02:00
a5a121ee00 flag for whether autodetection is currently active 2022-07-30 23:07:33 +02:00
459a0eaa91 build lied? 2022-07-30 00:55:20 +02:00
84fefbaa14 remove some console.logs 2022-07-30 00:54:12 +02:00
023b8c370a fix tslint complaints 2022-07-30 00:52:14 +02:00
0a023ac6f2 fix command 2022-07-30 00:47:11 +02:00
83f7e4498a npm audit fix maybe? 2022-07-30 00:32:23 +02:00
d57040fc5b Merge branch 'master' into feature/player-ui 2022-07-30 00:29:23 +02:00
7df62ba530 Version bump + changelog 2022-07-30 00:15:15 +02:00
f51ec50113 fix aard, attempt 3 2022-07-29 00:42:35 +02:00
6748cbe52d update package.lock 2022-07-29 00:25:29 +02:00
fda2dcc484 fix aard: fix harder ? 2022-07-29 00:25:16 +02:00
07a5f06695 Merge branch 'dependabot/npm_and_yarn/shell-quote-1.7.3' 2022-07-29 00:21:52 +02:00
c9392911d9 fix aard? 2022-07-29 00:21:34 +02:00
88dec55116 get DRM status in player ui 2022-07-28 00:45:35 +02:00
f30de3f4a0 Add debug panel with logger 2022-07-27 01:16:14 +02:00
3c5aafee2d update package.lock 2022-07-26 23:55:39 +02:00
52b4e48217 start extension popup rework 2022-07-26 23:54:25 +02:00
58ed3b1ca4 Update changelog 2022-07-26 22:41:12 +02:00
bfef49bc33 fix stuff from merge 2022-07-26 22:40:45 +02:00
b7a61e5bc0 Rework blackframe to not use its own drawImage call 2022-07-26 22:15:39 +02:00
dependabot[bot]
15a3ffd406 Bump shell-quote from 1.7.2 to 1.7.3
Bumps [shell-quote](https://github.com/substack/node-shell-quote) from 1.7.2 to 1.7.3.
- [Release notes](https://github.com/substack/node-shell-quote/releases)
- [Changelog](https://github.com/substack/node-shell-quote/blob/master/CHANGELOG.md)
- [Commits](https://github.com/substack/node-shell-quote/compare/v1.7.2...1.7.3)

---
updated-dependencies:
- dependency-name: shell-quote
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-07-26 19:59:10 +00:00
66f934e46e fix version but reals 2022-07-26 21:57:22 +02:00
47b85cfebc Revert "Bump node-sass from 4.14.1 to 7.0.0"
This reverts commit 9a3bd3ff88.
2022-07-26 01:24:33 +02:00
d0b060f31a Changelog + fix version bump 2022-07-26 01:20:29 +02:00
a756a9dc16 Version bump 2022-07-26 01:13:25 +02:00
207b82cc8b Fix set-extension-commnand for people who installed extension while this command was misspelled 2022-07-26 01:12:42 +02:00
26df9633b4 Remove ancient migrations 2022-07-26 01:10:11 +02:00
2b749d6a3b fix set-extension-mode commands 2022-07-26 01:04:36 +02:00
8c403f106c Merge remote-tracking branch 'origin/dependabot/npm_and_yarn/terser-4.8.1' 2022-07-26 00:52:25 +02:00
36aa226e9c Reimplement blackframe. Start timing blackframe execution. Add some placeholder UI for autodetection settings 2022-07-26 00:52:06 +02:00
dependabot[bot]
1946b77285 Bump terser from 4.8.0 to 4.8.1
Bumps [terser](https://github.com/terser/terser) from 4.8.0 to 4.8.1.
- [Release notes](https://github.com/terser/terser/releases)
- [Changelog](https://github.com/terser/terser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/terser/terser/commits)

---
updated-dependencies:
- dependency-name: terser
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-07-20 14:48:52 +00:00
a09ea26b0e Rework blackframe to not use its own drawImage call 2022-07-13 00:22:16 +02:00
1556506bd7 monitor autodetection performance 2022-07-13 00:21:21 +02:00
f94f6ec956 Autodetect script can now report performance data 2022-06-28 22:34:47 +02:00
aae566b82a stuff we missed in previous cleanup 2022-06-28 22:34:20 +02:00
dd8aeefdfd Clean up some performance stuff for autodetection 2022-06-28 20:53:50 +02:00
7d951a5115 comment cleanup 2022-06-28 19:28:14 +02:00
556722693c Rearrange things a bit. No longer do fallback mode 2022-06-28 01:33:21 +02:00
78f350b72b UI design for player selection is mostly done 2022-06-16 00:07:03 +02:00
340ee05443 fix pointer events 2022-06-15 00:53:43 +02:00
561a1aa5a5 Add more styling, choose few quick actions 2022-06-15 00:43:36 +02:00
e2fa4f9cfc some things weren't as "no longer needed" as initially thought 2022-06-14 23:23:08 +02:00
ee56187c9d give player section an icon 2022-06-14 23:20:26 +02:00
d3dbceb749 fix 2022-06-14 23:20:07 +02:00
799c953b30 Remove code no longer needed 2022-06-14 00:27:37 +02:00
e766d6da69 Allow users to set player element from the popup 2022-06-14 00:26:06 +02:00
53ed81fa81 showing and hiding of ultrawidify UI 2022-06-11 15:40:36 +02:00
320d35d9e5 holy shit we did it 🅱️ois, we made some progress on improving manual player detection 2022-06-10 00:22:06 +02:00
a1bfaaaf92 wip 2022-06-09 17:41:04 +02:00
03ebc87aa5 Prepare rendering of basic element tree 2022-06-09 09:01:30 +02:00
d2447d2d8a player detect panel boilerplate 2022-06-09 01:29:26 +02:00
296f146835 Refactor player detection + have player detection log some data for use in settings window 2022-06-09 01:28:46 +02:00
9a7e38d897 dict update 2022-06-09 01:27:50 +02:00
f0fcf996c9 set default stretching and cropping for site and extension in UI 2022-05-25 00:18:54 +02:00
c1b14979a2 also get alignment to work from player UI 2022-05-24 23:48:26 +02:00
9e9fdfe37f get zoom to work again 2022-05-24 23:45:41 +02:00
135ce2dfdd Merge remote-tracking branch 'origin/dependabot/npm_and_yarn/async-2.6.4' 2022-05-06 00:28:52 +02:00
167d46ca76 Merge remote-tracking branch 'origin/dependabot/npm_and_yarn/minimist-1.2.6' 2022-05-06 00:28:47 +02:00
23d2c252fe Merge remote-tracking branch 'origin/dependabot/npm_and_yarn/object-path-0.11.8' 2022-05-06 00:28:43 +02:00
9100c1d9de Merge remote-tracking branch 'origin/dependabot/npm_and_yarn/node-fetch-2.6.7' 2022-05-06 00:28:38 +02:00
b45b232ecf Merge remote-tracking branch 'origin/dependabot/npm_and_yarn/path-parse-1.0.7' 2022-05-06 00:28:35 +02:00
9648d30aa2 If videoData is destroyed, remove its eventBus from pageInfo's downstream buses 2022-05-06 00:28:13 +02:00
5a04c2eeee Migrate actionHandler to new commands 2022-05-06 00:23:15 +02:00
74fe85f3a2 Put a parent event bus to pageInfo 2022-05-06 00:23:01 +02:00
4b44efe6fc Finish event bus propagation 2022-05-06 00:22:35 +02:00
dependabot[bot]
8922054c44 Bump async from 2.6.3 to 2.6.4
Bumps [async](https://github.com/caolan/async) from 2.6.3 to 2.6.4.
- [Release notes](https://github.com/caolan/async/releases)
- [Changelog](https://github.com/caolan/async/blob/v2.6.4/CHANGELOG.md)
- [Commits](https://github.com/caolan/async/compare/v2.6.3...v2.6.4)

---
updated-dependencies:
- dependency-name: async
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-29 00:39:55 +00:00
302353f448 Style dropdowns 2022-04-13 23:03:55 +02:00
3891d946e0 Add new aspect ratios. Fix deleting old aspect ratios. 2022-04-13 21:15:33 +02:00
e92e2e22f5 Update and delete keyboard shortcuts 2022-04-13 01:17:25 +02:00
4f4eb46a60 Clear keyboard shortcut 2022-04-13 00:24:10 +02:00
0daef615c1 Sort out inputs, shortcut button 2022-04-12 00:48:33 +02:00
6ce8bba6b7 Start working on the edit box for crop settings 2022-04-09 15:58:40 +02:00
09cf3ce54e Reorder some methods around into logical sections 2022-04-09 15:37:23 +02:00
872b0dea6e Change 'official' to 'verified' 2022-03-29 02:01:03 +02:00
376f20aeab Show last selected aspect ratio in the UI 2022-03-29 01:53:16 +02:00
57aae96781 Update button css 2022-03-28 23:57:34 +02:00
7c27c33aa0 Prepare some things for edit mode 2022-03-28 23:57:25 +02:00
7dc354509b Remove some of dem pointless console.logs that are no longer needed 2022-03-28 23:56:52 +02:00
7c09440084 UI base: sort out header and tabs 2022-03-28 23:14:52 +02:00
dependabot[bot]
5538ec4d3d Bump minimist from 1.2.5 to 1.2.6
Bumps [minimist](https://github.com/substack/minimist) from 1.2.5 to 1.2.6.
- [Release notes](https://github.com/substack/minimist/releases)
- [Commits](https://github.com/substack/minimist/compare/1.2.5...1.2.6)

---
updated-dependencies:
- dependency-name: minimist
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-24 16:24:35 +00:00
89168b643c Connect popup (video settings only so far) to content script logic 2022-03-22 01:23:50 +01:00
7fefe255ea Add some things we missed in initial implementation of pointer-events toggling 2022-03-22 01:23:15 +01:00
3423aac49a remove some console.logs 2022-03-22 01:21:18 +01:00
dependabot[bot]
cc8d1d8a94 Bump node-fetch from 2.6.1 to 2.6.7
Bumps [node-fetch](https://github.com/node-fetch/node-fetch) from 2.6.1 to 2.6.7.
- [Release notes](https://github.com/node-fetch/node-fetch/releases)
- [Commits](https://github.com/node-fetch/node-fetch/compare/v2.6.1...v2.6.7)

---
updated-dependencies:
- dependency-name: node-fetch
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-20 23:52:29 +00:00
dependabot[bot]
c0a863740a Bump object-path from 0.11.5 to 0.11.8
Bumps [object-path](https://github.com/mariocasciaro/object-path) from 0.11.5 to 0.11.8.
- [Release notes](https://github.com/mariocasciaro/object-path/releases)
- [Commits](https://github.com/mariocasciaro/object-path/commits/v0.11.8)

---
updated-dependencies:
- dependency-name: object-path
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-20 23:52:23 +00:00
dependabot[bot]
f006eed113 Bump path-parse from 1.0.6 to 1.0.7
Bumps [path-parse](https://github.com/jbgutierrez/path-parse) from 1.0.6 to 1.0.7.
- [Release notes](https://github.com/jbgutierrez/path-parse/releases)
- [Commits](https://github.com/jbgutierrez/path-parse/commits/v1.0.7)

---
updated-dependencies:
- dependency-name: path-parse
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-20 23:52:22 +00:00
3f131f44d1 Merge remote-tracking branch 'origin/dependabot/npm_and_yarn/prismjs-1.27.0' 2022-03-21 00:51:22 +01:00
5cfc089bf1 Merge remote-tracking branch 'origin/dependabot/npm_and_yarn/ajv-6.12.6' 2022-03-21 00:51:20 +01:00
987b1ad655 Merge remote-tracking branch 'origin/dependabot/npm_and_yarn/node-sass-7.0.0' 2022-03-21 00:51:16 +01:00
1cd06040da Toggle 'pointer-events' based on whether mouse hovers over a csui element or not 2022-03-21 00:50:54 +01:00
f99220bf8b Process messages from content script, respond with results 2022-03-21 00:50:03 +01:00
8bef1d0c99 something's fucky with window.parent.location 2022-03-20 20:56:29 +01:00
47a73cfa6c Get page URL from window.parent.location instead of window.location for iframe 2022-03-20 20:44:50 +01:00
2dd901b1d5 Mark ui as clickable 2022-03-20 20:43:49 +01:00
46725c6fa4 Send mouse coordinates to the iframe 2022-03-20 20:40:11 +01:00
dependabot[bot]
adfc3fca5f Bump prismjs from 1.23.0 to 1.27.0
Bumps [prismjs](https://github.com/PrismJS/prism) from 1.23.0 to 1.27.0.
- [Release notes](https://github.com/PrismJS/prism/releases)
- [Changelog](https://github.com/PrismJS/prism/blob/master/CHANGELOG.md)
- [Commits](https://github.com/PrismJS/prism/compare/v1.23.0...v1.27.0)

---
updated-dependencies:
- dependency-name: prismjs
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-26 02:33:31 +00:00
dependabot[bot]
077610820c Bump ajv from 6.10.2 to 6.12.6
Bumps [ajv](https://github.com/ajv-validator/ajv) from 6.10.2 to 6.12.6.
- [Release notes](https://github.com/ajv-validator/ajv/releases)
- [Commits](https://github.com/ajv-validator/ajv/compare/v6.10.2...v6.12.6)

---
updated-dependencies:
- dependency-name: ajv
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-12 13:52:45 +00:00
dependabot[bot]
9a3bd3ff88 Bump node-sass from 4.14.1 to 7.0.0
Bumps [node-sass](https://github.com/sass/node-sass) from 4.14.1 to 7.0.0.
- [Release notes](https://github.com/sass/node-sass/releases)
- [Changelog](https://github.com/sass/node-sass/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sass/node-sass/compare/v4.14.1...v7.0.0)

---
updated-dependencies:
- dependency-name: node-sass
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-10 19:16:57 +00:00
85fed9d4c5 Hot dang, icons now show up in Chrome as well.
Also cleaned up some of the mess.
2022-01-29 16:23:40 +01:00
5a2d8d22cb Move extension popup into an iframe, get it to display at least something 2022-01-07 00:50:58 +01:00
761f2c21a8 fixes 2021-11-25 22:16:48 +01:00
c34340d0d1 install mdi font 2021-11-25 22:16:16 +01:00
045cd5a528 Stop using some of the old code 2021-11-25 20:00:32 +01:00
27b0609d34 Let's try to avoid injecting ultrawidify-specific CSS before we actually need it. Part 1/? 2021-11-25 00:31:38 +01:00
65556d9cad We will need to distinguish between two automatic aspect ratio types
AspectRatioType.Automatic: "start autodetection pls"
AspectRatioType.AutomaticUpdate: "Hi I'm aard and I want to change this aspect ratio to something new"
2021-11-25 00:14:06 +01:00
30835e94c0 UI: can set default crop, stretch. (TODO: actually get and use those values) 2021-11-23 01:32:55 +01:00
ee8ac1d9ee Site support level works now ... for most values of work 2021-11-21 02:08:53 +01:00
2be44d54ae Try achieving css class rename some more 2021-11-21 02:08:32 +01:00
cf8bedfad9 fix an oversight 2021-11-20 20:56:49 +01:00
5dc9b498d4 sample jenkins test 2021-11-13 00:57:41 +01:00
fb7a168d31 whitespace 2021-11-13 00:55:17 +01:00
484db037c0 use event bus 2021-11-13 00:54:20 +01:00
15a89583a2 Package updates 2021-11-13 00:48:08 +01:00
75cc0d5a87 handle alignment along y 2021-11-03 00:08:34 +01:00
0e3b962a00 whitespace 2021-11-02 22:20:01 +01:00
afaf65a53d Fix stretch mode from ui 2021-11-02 22:19:50 +01:00
f00d3b5aaf Fix zoom, but for realses 2021-11-02 20:52:01 +01:00
03bd442783 do the version bumping thing again 2021-11-02 20:51:45 +01:00
787bd1a532 Prevent eventBus from crashing everything under certain conditions 2021-11-01 01:18:07 +01:00
bd5befc18c Get sliders to update with values from aspect ratio options 2021-11-01 01:13:13 +01:00
829047585a Get manual zoom to work 2021-11-01 00:11:14 +01:00
78da45d468 cosmetic stuff 2021-10-31 23:18:44 +01:00
7f7ab7b752 Add some things to mitigate windows/wsl suckiness 2021-10-31 19:03:22 +01:00
79fb10c556 Add new format for defining actions. They're called commands now. 2021-10-28 22:53:43 +02:00
ffd9c6fb7a ExtConfPatches to typescrtipt 2021-10-28 22:53:23 +02:00
4074774d3a Preemtive version bump and patch notes 2021-10-27 20:46:14 +02:00
e7d43c566f Fix aspect ratio not getting applied correctly 2021-10-27 20:10:55 +02:00
52b94003bf Fix popup not opening 2021-10-27 20:10:21 +02:00
13bd280062 Get event bus to work (for some values of work) 2021-10-26 23:16:11 +02:00
209e4221d2 Start adding event bus 2021-10-26 22:20:50 +02:00
c4832a9ef7 Comment out temporarily unused commands 2021-10-26 20:11:26 +02:00
6cb8cc174b Prevent multiple concurrent vue app instances 2021-10-26 20:11:03 +02:00
e9b882376c Simplify player data dimension change tracking a bit 2021-10-26 00:30:38 +02:00
a4948d3eef Whitespace 2021-10-25 23:13:35 +02:00
746a78577e UI for manual zoom and video alignment 2021-10-24 00:57:57 +02:00
268ef7dae3 push the popup thing a bit further 2021-10-22 00:31:04 +02:00
6c1d666fdf switch bootstrap icons with mdijs 2021-10-22 00:30:36 +02:00
53fb3e8f46 Add stretch actions to the popup, styling 2021-10-19 22:49:56 +02:00
2df0b1a0fb formatting 2021-10-19 22:49:28 +02:00
3846d1a926 Cropping now also works from in-player UI 2021-10-19 22:49:13 +02:00
2a747e11af Merge branch 'master' into feature/player-ui 2021-10-19 20:27:41 +02:00
cda6b4e10d Version bump n shit 2021-09-19 21:32:47 +02:00
502ce707e1 Style 2021-09-19 21:22:12 +02:00
5b1f73a69e Fix problem with autodetection 2021-09-19 21:22:05 +02:00
f7eba0974f Fix default settings 2021-09-19 21:21:23 +02:00
2ae84bccd8 update changelog 2021-09-15 03:13:02 +02:00
3ac3f1aee5 It would be good to disable frame check if AARD is halted or exited 2021-09-15 01:45:51 +02:00
23ca74afdb Add quick readme to the logger 2021-09-15 00:54:46 +02:00
7d894fb1e3 it is higly recommended to actually resolve those promises 2021-09-15 00:54:23 +02:00
b39e6be952 Add a directory for debugging resources (e.g. logs submitted by users).
Since logs can be rather extensive and, more importantly, may accidentally contain PI, we also have them on gitignore.
2021-09-14 23:25:57 +02:00
415ebf6821 Simplify video rescans 2021-09-14 23:22:59 +02:00
b1ec4f7387 Make framecheck ever so slightly async 2021-09-14 19:12:54 +02:00
820af9b133 Patch for netflix 2021-09-13 22:47:27 +02:00
54c88dcb2f we no longer have gitlab 2021-08-28 02:24:23 +02:00
104ed85d72 Get UI to show again 2021-08-26 01:34:59 +02:00
84de5927cd Try initializing UI again 2021-08-26 01:07:39 +02:00
1b829e095b Merge branch 'master' into feature/player-ui 2021-08-26 00:54:07 +02:00
cbb4624e25 Version bump stuff 2021-08-26 00:45:22 +02:00
2cb02ff3f2 fix 2021-08-26 00:45:12 +02:00
b8cd441109 misc fixes 2021-08-26 00:37:56 +02:00
86a8c23999 Change how halting AARD is handled 2021-08-25 23:13:02 +02:00
a2573b5aaf Add performance timing to ar detector + make it use animationFrame 2021-08-25 22:32:39 +02:00
ea8671f64b Tidy up ArDetector 2021-08-25 20:39:27 +02:00
a234903b8f Fix logging popup a bit 2021-08-24 02:13:05 +02:00
981da95cde The logger should be working again? 2021-08-24 00:57:29 +02:00
18fe17f00f Youtube: fix alignment issues with square-ish videos in normal player (#162). Fix issue with misalignment of featured videos on channel page 2021-08-17 19:39:00 +02:00
5ad8302084 Add missing conf for piped.kavin.rocks (fix #151) 2021-07-14 22:45:00 +02:00
cfcd0c5945 version bump 2021-07-14 22:06:50 +02:00
eb1b1cf73d changelog 2021-07-14 22:06:42 +02:00
95d3d962f4 I reckon we can drop ExtConfPatches for versions over a year old
like, everyone should have receiving this and future updates should be on 5.x, or at least on 4.5.x
2021-07-14 21:19:39 +02:00
6eb96f917d Goodies for #160 2021-07-14 21:17:32 +02:00
5847215097 Merge branch 'master' into feature/player-ui 2021-07-05 01:22:30 +02:00
d102a8cd7d fix popup 2021-07-05 01:17:10 +02:00
571ac92cd3 Version bump & changelog 2021-07-05 01:02:21 +02:00
246743203f whitespace + plex rule 2021-07-05 00:51:15 +02:00
1cc76ab3b1 Improve UI in advanced settings 2021-07-05 00:49:35 +02:00
5c9112a07b package.json: add npm run dev-[browser] scripts 2021-07-05 00:48:59 +02:00
d92b24a2f6 Merge branch 'master' into feature/player-ui 2021-06-13 02:19:39 +02:00
7729bf545e shut up some of dem vue warnings 2021-06-13 02:19:05 +02:00
c14aa99e5b Fix extconfpatches 2021-06-13 02:18:47 +02:00
2ced591ed4 Fix build errors 2021-06-12 23:23:47 +02:00
56dcd0feb3 Merge branch 'master' into feature/player-ui 2021-06-12 23:22:54 +02:00
c43b1daa42 Edge probably doesn't need the 'pls ensure you're up to date' warning anymore 2021-06-12 23:21:01 +02:00
5e0f7c6160 update package.json — npm start now builds for _all_ browsers 2021-06-12 23:12:04 +02:00
cbaa424fc4 Version bumps and changelogs 2021-06-07 22:16:03 +02:00
c2039da7c8 Make some labels less confusing 2021-06-07 21:57:40 +02:00
6d4b161b06 Fix for disneyplus maybe? 2021-06-07 21:56:57 +02:00
fef2c26cbe raise RAM, second attempt 2021-06-07 20:34:27 +02:00
50719113b1 Raise memory limits in build scripts. Also build edge version on the CI 2021-06-07 20:09:35 +02:00
4e407e2cf8 Edge is supported for a while now, actually. 2021-06-07 19:55:05 +02:00
336879a4a1 Version bump & changelog 2021-05-12 00:19:21 +02:00
7fd28616aa Fix incorrect stretch with fix-source stretch 2021-05-12 00:01:40 +02:00
fba6a8d604 Additional details in the popup 2021-05-11 22:45:08 +02:00
e5c1bcded1 Safety check 2021-05-11 22:44:24 +02:00
b99929063c Restore aspect ratio if offsets fail verification 2021-05-11 22:43:27 +02:00
18d7414b90 refactor 2021-05-11 22:42:51 +02:00
Tamius Han
4427844a41 Merge pull request #154 from tamius-han/dependabot/npm_and_yarn/lodash-4.17.21
Bump lodash from 4.17.20 to 4.17.21
2021-05-11 14:49:50 +02:00
dependabot[bot]
e92f53f590 Bump lodash from 4.17.20 to 4.17.21
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.20 to 4.17.21.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.20...4.17.21)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-10 14:38:04 +00:00
965a48c086 Remove some code that was only needed cos old edge 2021-04-12 20:54:42 +02:00
0edd82479e Put in console.errors on extension initialization. 2021-04-12 20:54:26 +02:00
338b219142 Since webExt polyfill ensures 'browser' is also present in Chrome, code for opening settings page from popup stopped working in Chrome 2021-04-12 20:36:42 +02:00
c504d5594c Version bump 2021-04-12 20:31:50 +02:00
9beeffc679 Force old chrome installs to force-enable the zoom limit 2021-04-12 20:30:42 +02:00
19e021ce8e Update 'what's new'? 2021-04-12 20:27:49 +02:00
c863447f3d ; 2021-04-12 20:25:44 +02:00
520b0b7ade Extra comments 2021-04-12 19:08:12 +02:00
48e12c8504 Fix player detection on videos wider than the monitor 2021-04-12 19:03:18 +02:00
60f940f4db try/catch in aspect ratio getter 2021-04-12 19:01:28 +02:00
0bed840b6f fix settings interface 2021-04-10 04:17:07 +02:00
ddbddc45a9 Whitespace and stuff 2021-04-10 04:10:22 +02:00
a039fd4ce4 Make isFullscreen a bit more lenient.
innerWidth|Height and screen.width|height no longer need an exact match
2021-04-10 04:10:12 +02:00
d3c3c99069 fix 'is extension enabled' query 2021-04-10 04:09:00 +02:00
843a405323 get settings for current site function 2021-04-10 04:08:35 +02:00
cb548c3a3d aspect ratio is a getter now. Use said getter 2021-04-10 04:08:09 +02:00
d406438527 Enable zoom limit for new installs in Chrome as well 2021-04-10 04:03:14 +02:00
6737061713 Add new settings option 2021-04-10 04:02:52 +02:00
0bc1254ce0 Merge branch 'master' into feature/player-ui 2021-04-04 03:59:08 +02:00
190737b915 Merge branch 'master' into feature/player-ui 2021-04-01 23:50:08 +02:00
038191d38e Merge branch 'master' into feature/player-ui 2021-03-06 04:07:03 +01:00
84da78e2e8 expose videoData objects via window.ultrawidify 2021-01-27 00:41:42 +01:00
13b89eb2a9 Add sample button from old config 2021-01-27 00:41:13 +01:00
89c524a690 add video settings placeholder to the UI 2021-01-21 00:21:15 +01:00
d44d1b6e64 add getRuntime() to BrowserDetect 2021-01-21 00:20:43 +01:00
8ea5b3d5be add toString() method to an enum 2021-01-21 00:20:27 +01:00
f8c774700e Merge branch 'master' into feature/player-ui 2021-01-20 22:58:40 +01:00
5987fda0b2 Merge branch 'master' into feature/player-ui 2020-12-29 20:36:06 +01:00
0e0eb57fd9 Merge branch 'master' into feature/player-ui 2020-12-29 18:33:54 +01:00
93b8049b01 Move debug data into a separate component/panel 2020-12-17 01:49:28 +01:00
5cf74b4405 Fix typo in one of the mutations 2020-12-17 01:49:15 +01:00
2aa91df820 Add additional data to debug popup 2020-12-17 01:48:45 +01:00
85fedbfeaf Fix base css 2020-12-17 01:48:29 +01:00
1cbe74252c display some data 2020-12-16 01:40:09 +01:00
9fb9667dba fix base css 2020-12-16 01:36:00 +01:00
55b6ca7392 fix comms 2020-12-16 01:35:39 +01:00
758c2bf0bc Report data to the UI 2020-12-16 00:19:37 +01:00
6eed271814 Display such data in a very rudamentary way 2020-12-16 00:19:20 +01:00
7a5a4bddf8 support passing debug data between content script and UI 2020-12-16 00:19:02 +01:00
3f8ea683ef Add toString() method to selected enums 2020-12-16 00:18:23 +01:00
29dff35bb4 Merge branch 'master' into feature/player-ui 2020-12-15 21:57:03 +01:00
79462c320d Add anchor for player UI 2020-12-15 00:26:19 +01:00
286 changed files with 44755 additions and 52681 deletions

2
.gitignore vendored
View File

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

View File

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

2
.nvmrc
View File

@ -1 +1 @@
14.4.0
20.19.6

18
.vscode/launch.json vendored
View File

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

43
.vscode/settings.json vendored
View File

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

View File

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

39
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,39 @@
# Contributing to Ultrawidify
Thank you for considering contributing to this project! We welcome contributions from the community and are grateful for your efforts.
## How to Contribute
0. Open an issue, where you introduce the feature you want to add or thing you want to fix
1. Fork the repository.
2. Create a new branch for your contribution.
3. Write readable code
4. Check that your additions do not break existing features
5. Open a pull request
## Code of Conduct
Please follow the code of conduct:
* don't be too much of an asshole
* language policing my code comments is not a worthwhile contribution, and is considered spam. If you're here to do that, fuck off. I'm looking at you, ScamOSS and co.
## Code Style
- Use formatting consistent with the rest of the project
- Write meaningful commit messages.
- Include comments where necessary to explain complex logic.
## Licensing & Contribution Agreement
By submitting a contribution (e.g., via pull request), you agree to the following:
- You grant the project maintainer(s) a non-exclusive, irrevocable, worldwide, royalty-free license to use, copy, modify, and distribute your contributions as part of the project.
- You certify that your contributions are your original work and that you have the right to submit them.
- You agree that your contributions are licensed under the same license as the rest of the project, unless explicitly stated otherwise.
**TL;DR:** once your code is in the project, there's no take-backsies.
## Questions?
The discussion board is right over there, three tabs to the right.

6
DOCUMENTATION.MD Normal file
View File

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

32
Jenkinsfile vendored Normal file
View File

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

35
LICENSE.MD Normal file
View File

@ -0,0 +1,35 @@
# License
Copyright (c) 2025 @tamius-han
The source code is provided on the "you can look, but you can't take" basis.
## Permissions
You are permitted to:
* You can view the code
* You can create forks and modify the code for personal use
* You can build and run modified versions of this project for personal use on your local machine
* Push modifications to your personal github fork
## Restrictions
You may NOT:
* Distribute this project or any modified versions outside of your personal Github fork
* Publish or distribute compiled binaries, builds, or packages of this project or any derivative work
* Use the code in commercial products, services, or other public-facing deployments.
* Sub-license, sell, or otherwise make the software or derivatives available to third parties.
## Contributions
By submitting a pull request or other contribution, you agree that:
- You grant the project maintainer(s) a non-exclusive, irrevocable, worldwide, royalty-free license to use, copy, modify, and distribute your contributions as part of the project.
- You certify that your contributions are your original work and that you have the right to submit them.
- You agree that your contributions are licensed under the same license as the rest of the project, unless explicitly stated otherwise.
## Disclaimer
This software is provided "as is", without warranty of any kind, express or implied. Use at your own risk.

View File

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

55615
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

10
paths.config.js Normal file
View File

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

24
postcss.config.js Normal file
View File

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

View File

@ -4,14 +4,33 @@
# command from the root directory of the project. Running it in
# any other way probably isn't going to work.
# pre-build steps:
# pre-build steps:
echo "—————————————————————————————————————————————————————";
echo " BUILDING ALL ULTRAWIDIFY VERSIONS";
echo "—————————————————————————————————————————————————————";
mkdir -p ./build/old
npm run pre-build
echo "::"
echo "::"
echo "::"
echo ":: Pre-build complete. Moving old builds from dist-zip ..."
rm ./dist-zip/uw-amo-source.zip
mv -f ./dist-zip/*.zip ./build/old
echo "::"
echo "::"
echo "::"
echo ":: dist-zip is clean. Starting builds ..."
# lets force raise ram limit, but the improper way
# export NODE_OPTIONS=--max_old_space_size=4096
# build the version for each browser and create a zip afterwards
# step 1: define build functions
# step 1: define build functions
#function buildFF {
npm run build
node scripts/build-zip.js ff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

@ -1,90 +0,0 @@
<template>
<div class="flex flex-row json-level-indent">
<div>
<b>
<span v-if="label" class="item-key"
:class="{'item-key-boolean-false': typeof value_internal === 'boolean' && !value_internal}"
>
"{{label}}"
</span>
:&nbsp;
</b>
</div>
<div v-if="typeof value_internal === 'boolean'"
:class="{'json-value-boolean-true': value_internal, 'json-value-boolean-false': !value_internal}"
@click="toggleBoolean"
>
<template v-if="value_internal">true</template>
<template v-else>false</template>
</div>
<div v-else
class="flex flex-row inline-edit"
:class="{
'json-value-number': typeof value_internal === 'number',
'json-value-string': typeof value_internal === 'string'
}"
>
<template v-if="typeof value_internal === 'string'">
"<div ref="element-edit-area"
contenteditable="true"
@keydown.enter="changeValue"
>
{{value_internal}}
</div>"
</template>
<template v-else>
<div ref="element-edit-area"
contenteditable="true"
@keydown.enter="changeValue"
>
{{value_internal}}
</div>
</template>,
</div>
</div>
</template>
<script>
export default {
name: 'json-element',
props: [
'value',
'label',
],
data() {
return {
value_internal: undefined,
editing: false,
}
},
created() {
this.value_internal = this.value;
},
watch: {
value: function(val) {
this.value_internal = val;
}
},
methods: {
toggleBoolean() {
this.value_internal = !this.value_internal;
this.$emit('change', this.value_internal);
},
changeValue() {
this.$refs['element-edit-area'].blur();
const newValue = this.$refs['element-edit-area'].textContent.replace(/\n/g, '');
if (isNaN(newValue)) {
this.value_internal = newValue;
this.$emit('change', newValue);
} else {
this.value_internal = +newValue;
this.$emit('change', +newValue);
}
this.editing = false;
}
}
}
</script>
<style lang="scss" scoped src="./json.scss">
</style>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,160 +1,370 @@
import { Action } from '../../../node_modules/vuex/types/index'
import AntiGradientMode from '../enums/AntiGradientMode.enum'
import AspectRatioType from '../enums/AspectRatioType.enum'
import CropModePersistence from '../enums/CropModePersistence.enum'
import ExtensionMode from '../enums/ExtensionMode.enum'
import StretchType from '../enums/StretchType.enum'
import VideoAlignmentType from '../enums/VideoAlignmentType.enum'
import { AardPollingOptions } from '@src/ext/module/aard/enums/aard-polling-options.enum'
import { AardSubtitleCropMode } from '@src/ext/module/aard/enums/aard-subtitle-crop-mode.enum'
import { SiteSupportLevel } from '@src/common/enums/SiteSupportLevel.enum';
import AspectRatioType from '@src/common/enums/AspectRatioType.enum'
import CropModePersistence from '@src/common/enums/CropModePersistence.enum'
import EmbeddedContentSettingsOverridePolicy from '@src/common/enums/EmbeddedContentSettingsOverridePolicy.enum'
import ExtensionMode from '@src/common/enums/ExtensionMode.enum'
import StretchType from '@src/common/enums/StretchType.enum'
import VideoAlignmentType from '@src/common/enums/VideoAlignmentType.enum'
import { PlayerDetectionMode } from '@src/common/enums/PlayerDetectionMode.enum';
import { InputHandlingMode } from '@src/common/enums/InputHandlingMode.enum';
import { MenuPosition } from '@src/common/interfaces/ClientUiMenu.interface';
export enum ExtensionEnvironment {
Normal = ExtensionMode.All,
Theater = ExtensionMode.Theater,
Fullscreen = ExtensionMode.FullScreen,
}
export interface DevUiConfig {
aardDebugOverlay: {
showOnStartup: boolean,
showDetectionDetails: boolean,
}
}
export interface KeyboardShortcutInterface {
key?: string,
code?: string,
ctrlKey?: boolean,
metaKey?: boolean,
altKey?: boolean,
shiftKey?: boolean,
onKeyUp?: boolean,
onKeyDown?: boolean,
onMouseMove?: boolean,
}
interface ActionScopeInterface {
show: boolean,
label?: string, // example override, takes precedence over default label
shortcut?: {
key?: string,
code?: string,
ctrlKey?: boolean,
metaKey?: boolean,
altKey?: boolean,
shiftKey?: boolean,
onKeyUp?: boolean,
onKeyDown?: boolean,
onMouseMove?: boolean,
}[],
shortcut?: KeyboardShortcutInterface[],
}
interface SettingsInterface {
arDetect: {
disabledReason: string, // if automatic aspect ratio has been disabled, show reason
allowedMisaligned: number, // top and bottom letterbox thickness can differ by this much.
// Any more and we don't adjust ar.
allowedArVariance: number, // amount by which old ar can differ from the new (1 = 100%)
timers: { // autodetection frequency
playing: number, // while playing
paused: number, // while paused
error: number, // after error
minimumTimeout: number,
tickrate: number, // 1 tick every this many milliseconds
},
autoDisable: { // settings for automatically disabling the extension
maxExecutionTime: number, // if execution time of main autodetect loop exceeds this many milliseconds,
// we disable it.
consecutiveTimeoutCount: number, // we only do it if it happens this many consecutive times
interface RestrictionsSettings {
disableOnSmallPlayers?: boolean; // Whether ultrawidify should disable itself when the player is small
minAllowedWidth?: number; // if player is less than this many px wide, ultrawidify will disable itself
minAllowedHeight?: number; // if player is less than this many px tall, ultrawidify will disable itself
onlyAllowInFullscreen?: boolean; // if enabled, ultrawidify will be disabled when not in full screen regardless of what previous two options say
onlyAllowAutodetectionInFullScreen?: boolean; // if enabled, autodetection will only start once in full screen
}
// FOR FUTURE USE
consecutiveArResets: number // if aspect ratio reverts immediately after AR change is applied, we disable everything
},
canvasDimensions: {
blackframeCanvas: { // smaller than sample canvas, blackframe canvas is used to recon for black frames
// it's not used to detect aspect ratio by itself, so it can be tiny af
width: number,
height: number,
},
sampleCanvas: { // size of image sample for detecting aspect ratio. Bigger size means more accurate results,
// at the expense of performance
width: number,
height: number,
},
},
export interface CommandInterface {
action: string,
label: string,
comment?: string,
arguments?: any,
shortcut?: KeyboardShortcutInterface,
internalOnly?: boolean,
actionId?: string,
}
// samplingInterval: 10, // we sample at columns at (width/this) * [ 1 .. this - 1]
blackframe: {
sufficientColorVariance: number, // calculate difference between average intensity and pixel, for every pixel for every color
// component. Average intensity is normalized to where 0 is black and 1 is biggest value for
// that component. If sum of differences between normalized average intensity and normalized
// component varies more than this % between color components, we can afford to use less strict
// cumulative threshold.
cumulativeThresholdLax: number,
cumulativeThresholdStrict: number,// if we add values of all pixels together and get more than this, the frame is bright enough.
// (note: blackframe is 16x9 px -> 144px total. cumulative threshold can be reached fast)
blackPixelsCondition: number, // How much pixels must be black (1 all, 0 none) before we consider frame as black. Takes
// precedence over cumulative threshold: if blackPixelsCondition is met, the frame is dark
// regardless of whether cumulative threshold has been reached.
},
blackbar: {
blackLevel: number, // everything darker than 10/255 across all RGB components is considered black by
// default. blackLevel can decrease if we detect darker black.
threshold: number, // if pixel is darker than the sum of black level and this value, we count it as black
// on 0-255. Needs to be fairly high (8 might not cut it) due to compression
// artifacts in the video itself
frameThreshold: number, // threshold, but when doing blackframe test
imageThreshold: number, // in order to detect pixel as "not black", the pixel must be brighter than
// the sum of black level, threshold and this value.
gradientThreshold: number, // When trying to determine thickness of the black bars, we take 2 values: position of
// the last pixel that's darker than our threshold, and position of the first pixel that's
// brighter than our image threshold. If positions are more than this many pixels apart,
// we assume we aren't looking at letterbox and thus don't correct the aspect ratio.
gradientSampleSize: number, // How far do we look to find the gradient
maxGradient: number, // if two neighboring pixels in gradientSampleSize differ by more than this, then we aren't
// looking at a gradient
gradientNegativeTreshold: number,
gradientMaxSD: number, // reserved for future use
antiGradientMode: AntiGradientMode
},
variableBlackbarThresholdOptions: { // In case of poor bitrate videos, jpeg artifacts may cause us issues
// FOR FUTURE USE
enabled: boolean, // allow increasing blackbar threshold
disableArDetectOnMax: boolean, // disable autodetection when threshold goes over max blackbar threshold
maxBlackbarThreshold: number, // max threshold (don't increase past this)
thresholdStep: number, // when failing to set aspect ratio, increase threshold by this much
increaseAfterConsecutiveResets: number // increase if AR resets this many times in a row
},
sampling: {
staticCols: number, // we take a column at [0-n]/n-th parts along the width and sample it
randomCols: number, // we add this many randomly selected columns to the static columns
staticRows: number, // forms grid with staticSampleCols. Determined in the same way. For black frame checks
},
guardLine: { // all pixels on the guardline need to be black, or else we trigger AR recalculation
// (if AR fails to be recalculated, we reset AR)
enabled: boolean,
ignoreEdgeMargin: number, // we ignore anything that pokes over the black line this close to the edge
// (relative to width of the sample)
imageTestThreshold: number, // when testing for image, this much pixels must be over blackbarThreshold
edgeTolerancePx: number, // black edge violation is performed this far from reported 'last black pixel'
edgeTolerancePercent: null // unused. same as above, except use % of canvas height instead of pixels
},
fallbackMode: {
enabled: boolean,
safetyBorderPx: number, // determines the thickness of safety border in fallback mode
noTriggerZonePx: number // if we detect edge less than this many pixels thick, we don't correct.
},
arSwitchLimiter: { // to be implemented
switches: number, // we can switch this many times
period: number // per this period
},
edgeDetection: {
sampleWidth: number, // we take a sample this wide for edge detection
detectionThreshold: number, // sample needs to have this many non-black pixels to be a valid edge
confirmationThreshold: number, //
singleSideConfirmationThreshold: number, // we need this much edges (out of all samples, not just edges) in order
// to confirm an edge in case there's no edges on top or bottom (other
// than logo, of course)
logoThreshold: number, // if edge candidate sits with count greater than this*all_samples, it can't be logo
// or watermark.
edgeTolerancePx?: number, // we check for black edge violation this far from detection point
edgeTolerancePercent?: number, // we check for black edge detection this % of height from detection point. unused
middleIgnoredArea: number, // we ignore this % of canvas height towards edges while detecting aspect ratios
minColsForSearch: number, // if we hit the edge of blackbars for all but this many columns (%-wise), we don't
// continue with search. It's pointless, because black edge is higher/lower than we
// are now. (NOTE: keep this less than 1 in case we implement logo detection)
},
pillarTest: {
ignoreThinPillarsPx: number, // ignore pillars that are less than this many pixels thick.
allowMisaligned: number // left and right edge can vary this much (%)
},
textLineTest: {
nonTextPulse: number, // if a single continuous pulse has this many non-black pixels, we aren't dealing
// with text. This value is relative to canvas width (%)
pulsesToConfirm: number, // this is a threshold to confirm we're seeing text.
pulsesToConfirmIfHalfBlack: number, // this is the threshold to confirm we're seeing text if longest black pulse
// is over 50% of the canvas width
testRowOffset: number // we test this % of height from detected edge
}
export type SettingsReloadComponent = 'PlayerData' | 'VideoData';
export type SettingsReloadFlags = true | SettingsReloadComponent;
export interface AardSubtitleScanOptions {
subtitleCropMode: AardSubtitleCropMode,
resumeAfter: number, // resume after X ms of no subtitle
scanSpacing: number, // repeat subtitle scan every _n_ lines
scanMargin: number, // % of pixels (from edge to start area) that we skip while scanning.
// While technically anything between 0 and 0.5 is valid, the value
// should be somewhere between 0.1-0.3 in order for subtitle scan to
// work properly.
maxValidLetter: number, // if letter is longer than this, something's off.
subtitleSubpixelThresholdOn: number,
subtitleSubpixelThresholdOff: number,
minDetections: number,
minImageLineDetections: number,
maxPotentialSubtitleMisalignment: number, // how many pixels off-center can "potential subtitle" be
refiningScanSpacing: number, // must be base-2
refiningScanInitialIterations: number,
}
export interface AardSettings {
aardType: 'webgl' | 'legacy' | 'auto';
useLegacy: boolean,
earlyStopOptions: {
stopAfterFirstDetection: boolean;
stopAfterTimeout: boolean;
stopTimeout: number;
},
polling: {
runInBackgroundTabs: AardPollingOptions;
runOnSmallVideos: AardPollingOptions;
}
disabledReason: string, // if automatic aspect ratio has been disabled, show reason
allowedMisaligned: number, // top and bottom letterbox thickness can differ by this much.
// Any more and we don't adjust ar.
allowedArVariance: number, // amount by which old ar can differ from the new (1 = 100%)
timers: { // autodetection frequency
playing: number, // while playing
playingReduced: number, // while video/player element has insufficient size
paused: number, // while paused
error: number, // after error
minimumTimeout: number,
tickrate: number, // 1 tick every this many milliseconds
},
subtitles: AardSubtitleScanOptions,
autoDisable: { // settings for automatically disabling the extension
onFirstChange: boolean,
ifNotChanged: boolean,
ifNotChangedTimeout: number,
ifSubtitles: boolean;
},
canvasDimensions: {
blackframeCanvas: { // smaller than sample canvas, blackframe canvas is used to recon for black frames
// it's not used to detect aspect ratio by itself, so it can be tiny af
width: number,
height: number,
},
sampleCanvas: { // size of image sample for detecting aspect ratio. Bigger size means more accurate results,
// at the expense of performance
width: number,
height: number,
},
},
blackLevels: {
defaultBlack: number, // By default, pixels darker than this are considered black.
// (If detection algorithm detects darker blacks, black is considered darkest detected pixel)
blackTolerance: number, // If pixel is more than this much brighter than blackLevel, it's considered not black
// It is not considered a valid image detection if gradient detection is enabled
imageDelta: number, // When gradient detection is enabled, pixels this much brighter than black skip gradient detection
}
sampling: {
edgePosition: number; // % of width (max 0.33). Pixels up to this far away from either edge may contain logo.
staticCols: number, // we take a column at [0-n]/n-th parts along the width and sample it
randomCols: number, // we add this many randomly selected columns to the static columns
staticRows: number, // forms grid with staticSampleCols. Determined in the same way. For black frame checks,
},
// pls deprecate and move things used
edgeDetection: {
gradientThreshold: number, // if more than this percentage (0-1) is detected as gradient, we mark edge as gradient
gradientTestMinDelta: number, // if difference between test row and before row is MORE than this -> not gradient
gradientTestMinDeltaAfter: number, // if difference between test row and after row is LESS than this -> not gradient
gradientTestMaxDeltaAfter: number, // if difference between test row and after row is MORE than this -> not gradient
maxLetterboxOffset: number, // Upper and lower letterbox can be different by this many (% of height)
minValidImage: number, // if more than this % (0-1) of row is image, we confirm image regardless of other criteria except gradient
maxEdgeSegments: number, // if edge has more than this many segments, we consider it unreliable
minEdgeSegmentSize: number,
averageEdgeThreshold: number, // average edge must be this many px
},
letterboxOrientationScan: {
letterboxLimit: number, // how many non-black pixels we can detect before ruling out letterbox
pillarboxLimit: number, // how many non-black pixels we can detect before ruling out pillarbox
}
pillarTest: {
ignoreThinPillarsPx: number, // ignore pillars that are less than this many pixels thick.
allowMisaligned: number // left and right edge can vary this much (%)
},
textLineTest: {
nonTextPulse: number, // if a single continuous pulse has this many non-black pixels, we aren't dealing
// with text. This value is relative to canvas width (%)
pulsesToConfirm: number, // this is a threshold to confirm we're seeing text.
pulsesToConfirmIfHalfBlack: number, // this is the threshold to confirm we're seeing text if longest black pulse
// is over 50% of the canvas width
testRowOffset: number // we test this % of height from detected edge
}
}
export interface AardLegacySettings {
aardType: 'webgl' | 'legacy' | 'auto';
earlyStopOptions: {
stopAfterFirstDetection: boolean;
stopAfterTimeout: boolean;
stopTimeout: number;
},
polling: {
runInBackgroundTabs: AardPollingOptions;
runOnSmallVideos: AardPollingOptions;
}
disabledReason: string, // if automatic aspect ratio has been disabled, show reason
allowedMisaligned: number, // top and bottom letterbox thickness can differ by this much.
// Any more and we don't adjust ar.
allowedArVariance: number, // amount by which old ar can differ from the new (1 = 100%)
timers: { // autodetection frequency
playing: number, // while playing
playingReduced: number, // while video/player element has insufficient size
paused: number, // while paused
error: number, // after error
minimumTimeout: number,
tickrate: number, // 1 tick every this many milliseconds
},
subtitles: AardSubtitleScanOptions,
autoDisable: { // settings for automatically disabling the extension
onFirstChange: boolean,
ifNotChanged: boolean,
ifNotChangedTimeout: number,
ifSubtitles: boolean;
},
canvasDimensions: {
blackframeCanvas: { // smaller than sample canvas, blackframe canvas is used to recon for black frames
// it's not used to detect aspect ratio by itself, so it can be tiny af
width: number,
height: number,
},
sampleCanvas: { // size of image sample for detecting aspect ratio. Bigger size means more accurate results,
// at the expense of performance
width: number,
height: number,
},
},
blackLevels: {
defaultBlack: number, // By default, pixels darker than this are considered black.
// (If detection algorithm detects darker blacks, black is considered darkest detected pixel)
blackTolerance: number, // If pixel is more than this much brighter than blackLevel, it's considered not black
// It is not considered a valid image detection if gradient detection is enabled
imageDelta: number, // When gradient detection is enabled, pixels this much brighter than black skip gradient detection
}
sampling: {
edgePosition: number; // % of width (max 0.33). Pixels up to this far away from either edge may contain logo.
staticCols: number, // we take a column at [0-n]/n-th parts along the width and sample it
randomCols: number, // we add this many randomly selected columns to the static columns
staticRows: number, // forms grid with staticSampleCols. Determined in the same way. For black frame checks,
},
// pls deprecate and move things used
edgeDetection: {
slopeTestWidth: number,
gradientTestSamples: number, // we check this many pixels below (or above) the suspected edge to check for gradient
gradientTestBlackThreshold: number, // if pixel in test sample is brighter than that, we aren't looking at gradient
gradientTestDeltaThreshold: number, // if delta between two adjacent pixels in gradient test exceeds this, it's not gradient
thresholds: {
edgeDetectionLimit: number, // during scanning of the edge, quit after edge gets detected at this many points
minQualitySingleEdge: number, // At least one of the detected must reach this quality
minQualitySecondEdge: number, // The other edge must reach this quality (must be smaller or equal to single edge quality)
}
gradientThreshold: number, // if more than this percentage (0-1) is detected as gradient, we mark edge as gradient
gradientTestMinDelta: number, // if difference between test row and before row is MORE than this -> not gradient
gradientTestMinDeltaAfter: number, // if difference between test row and after row is LESS than this -> not gradient
gradientTestMaxDeltaAfter: number, // if difference between test row and after row is MORE than this -> not gradient
maxLetterboxOffset: number, // Upper and lower letterbox can be different by this many (% of height)
// Previous iteration variables VVVV
sampleWidth: number, // we take a sample this wide for edge detection
detectionThreshold: number, // sample needs to have this many non-black pixels to be a valid edge
confirmationThreshold: number, //
singleSideConfirmationThreshold: number, // we need this much edges (out of all samples, not just edges) in order
// to confirm an edge in case there's no edges on top or bottom (other
// than logo, of course)
logoThreshold: number, // if edge candidate sits with count greater than this*all_samples, it can't be logo
// or watermark.
edgeTolerancePx?: number, // we check for black edge violation this far from detection point
edgeTolerancePercent?: number, // we check for black edge detection this % of height from detection point. unused
middleIgnoredArea: number, // we ignore this % of canvas height towards edges while detecting aspect ratios
minColsForSearch: number, // if we hit the edge of blackbars for all but this many columns (%-wise), we don't
// continue with search. It's pointless, because black edge is higher/lower than we
// are now. (NOTE: keep this less than 1 in case we implement logo detection)
edgeMismatchTolerancePx: number,// corners and center are considered equal if they differ by at most this many px
minValidImage: number, // if more than this % (0-1) of row is image, we confirm image regardless of other criteria except gradient
maxEdgeSegments: number, // if edge has more than this many segments, we consider it unreliable
minEdgeSegmentSize: number,
averageEdgeThreshold: number, // average edge must be this many px
},
letterboxOrientationScan: {
letterboxLimit: number, // how many non-black pixels we can detect before ruling out letterbox
pillarboxLimit: number, // how many non-black pixels we can detect before ruling out pillarbox
}
pillarTest: {
ignoreThinPillarsPx: number, // ignore pillars that are less than this many pixels thick.
allowMisaligned: number // left and right edge can vary this much (%)
},
textLineTest: {
nonTextPulse: number, // if a single continuous pulse has this many non-black pixels, we aren't dealing
// with text. This value is relative to canvas width (%)
pulsesToConfirm: number, // this is a threshold to confirm we're seeing text.
pulsesToConfirmIfHalfBlack: number, // this is the threshold to confirm we're seeing text if longest black pulse
// is over 50% of the canvas width
testRowOffset: number // we test this % of height from detected edge
}
}
interface DevSettings {
loadFromSnapshot: boolean,
}
export interface InPlayerUIOptions {
activatorAlignment: MenuPosition,
minEnabledWidth: number, // don't show UI if player is narrower than % of screen width
minEnabledHeight: number, // don't show UI if player is narrower than % of screen height
activation: 'player' | 'trigger-zone' | 'distance' | 'none', // what needs to be hovered in order for UI to be visible
activateWithCtrl: boolean,
activationDistance: number,
activationDistanceUnits: '%' | 'px',
activatorPadding: {x: number, y: number}
activatorPaddingUnit: {x: '%' | 'px', y: '%' | 'px'},
triggerZoneDimensions: { // how large the trigger zone is (relative to player size)
width: number
height: number,
offsetX: number, // fed to translateX(offsetX + '%'). Valid range [-100, 0]
offsetY: number // fed to translateY(offsetY + '%'). Valid range [-100, 100]
},
};
interface SettingsInterface {
_updateFlags?: {
requireReload?: SettingsReloadFlags,
forSite?: string
}
dev: DevSettings,
aardLegacy: AardLegacySettings,
aard: AardSettings,
ui: {
inPlayer: InPlayerUIOptions,
devMode?: boolean,
dev: DevUiConfig,
}
restrictions?: RestrictionsSettings;
crop: {
default: any;
},
stretch: {
default: any;
conditionalDifferencePercent: number // black bars less than this wide will trigger stretch
// if mode is set to '1'. 1.0=100%
},
kbm: { // TODO: we dont use this anymore
enabled: boolean, // if keyboard/mouse handler service will run
keyboardEnabled: boolean, // if keyboard shortcuts are processed
mouseEnabled: boolean, // if mouse movement is processed
}
zoom: {
minLogZoom: number,
maxLogZoom: number,
announceDebounce: number // we wait this long before announcing new zoom
},
miscSettings: {
mousePan: {
enabled: boolean
@ -162,10 +372,7 @@ interface SettingsInterface {
mousePanReverseMouse: boolean,
defaultAr?: any
},
stretch: {
conditionalDifferencePercent: number // black bars less than this wide will trigger stretch
// if mode is set to '1'. 1.0=100%
},
resizer: {
setStyleString: {
maxRetries: number,
@ -181,6 +388,7 @@ interface SettingsInterface {
pan?: any,
version?: string,
preventReload?: boolean,
lastModified?: Date,
// -----------------------------------------
// ::: MITIGATIONS :::
@ -193,100 +401,125 @@ interface SettingsInterface {
limit?: number,
}
}
// -----------------------------------------
// ::: ACTIONS :::
// -----------------------------------------
// Nastavitve za ukaze. Zamenja stare nastavitve za bližnične tipke.
//
// Polje 'shortcut' je tabela, če se slučajno lotimo kdaj delati choordov.
actions: {
name?: string, // name displayed in settings
label?: string, // name displayed in ui (can be overridden in scope/playerUi)
cmd?: {
action: string,
arg: any,
customArg?: any,
persistent?: boolean, // optional, false by default. If true, change doesn't take effect immediately.
// Instead, this action saves stuff to settings
}[],
scopes?: {
global?: ActionScopeInterface,
site?: ActionScopeInterface,
page?: ActionScopeInterface
},
playerUi?: {
show: boolean,
path?: string,
},
userAdded?: boolean,
}[],
// This object fulfills the same purpose as 'actions', but is written in less retarded and overly
// complicated way. Hopefully it'll be easier to maintain it that way.
commands?: {
crop?: CommandInterface[],
stretch?: CommandInterface[],
zoom?: CommandInterface[],
pan?: CommandInterface[],
internal?: CommandInterface[],
},
whatsNewChecked: boolean,
newFeatureTracker: any,
// -----------------------------------------
// ::: 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.
// 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: {
[x: string]: {
mode?: ExtensionMode,
autoar?: ExtensionMode,
autoarFallback?: ExtensionMode,
stretch?: StretchType,
videoAlignment?: VideoAlignmentType,
keyboardShortcutsEnabled?: ExtensionMode,
type?: string,
override?: boolean,
arPersistence?: boolean,
actions?: any;
cropModePersistence?: CropModePersistence;
DOM?: {
player?: {
manual?: boolean,
querySelectors?: string,
additionalCss?: string,
useRelativeAncestor?: boolean,
videoAncestor?: any,
playerNodeCss?: string,
periodicallyRefreshPlayerElement?: boolean
},
video?: {
manual?: boolean,
querySelectors?: string,
additionalCss?: string,
useRelativeAncestor?: boolean,
playerNodeCss?: string
}
},
css?: string;
}
[x: string]: SiteSettingsInterface,
}
}
export default SettingsInterface;
export interface SiteSettingsInterface {
notes?: string; // any special things related to this site.
enable: ExtensionMode;
enableAard: ExtensionMode;
enableKeyboard: InputHandlingMode;
enableUI: ExtensionMode;
applyToEmbeddedContent: EmbeddedContentSettingsOverridePolicy; // presumed to be 'Use as default' if not defined
overrideWhenEmbedded?: EmbeddedContentSettingsOverridePolicy;
autocreated?: boolean;
type?: SiteSupportLevel;
defaultType: SiteSupportLevel;
// must be defined in @global and @empty
persistCSA?: CropModePersistence, // CSA - crop, stretch, alignment
defaults?: { // must be defined in @global and @empty
crop?: {type: AspectRatioType, [x: string]: any},
stretch?: {type: StretchType, ratio?: number},
alignment?: {x: VideoAlignmentType, y: VideoAlignmentType},
}
cropModePersistence?: CropModePersistence;
stretchModePersistence?: CropModePersistence;
alignmentPersistence?: CropModePersistence;
playerAutoConfig?: PlayerAutoConfigInterface;
activeDOMConfig?: string;
DOMConfig?: { [x: string]: SiteDOMSettingsInterface };
// the following script are for extension caching and shouldn't be saved.
// if they _are_ saved, they will be overwritten
currentDOMConfig?: SiteDOMSettingsInterface;
// the following fields are for use with extension update script
override?: boolean; // whether settings for this site will be overwritten by extension upgrade script
}
export interface PlayerAutoConfigInterface {
modified: boolean;
initialIndex: number;
currentIndex: number;
}
export interface SiteDOMSettingsInterface {
type: SiteSupportLevel; // 'official' | 'community' | 'user-defined' | 'modified' | undefined;
elements?: {
player?: PlayerDOMSettingsInterface,
video?: PlayerDOMSettingsInterface,
};
customCss?: string;
periodicallyRefreshPlayerElement?: boolean;
// the following script are for extension caching and shouldn't be saved.
// if they _are_ saved, they will be overwritten
anchorElementIndex?: number;
anchorElement?: HTMLElement;
}
export interface PlayerDOMSettingsInterface {
detectionMode: PlayerDetectionMode,
allowAutoFallback: boolean,
ancestorIndex?: number,
querySelectors?: string,
}
export interface SiteDOMElementSettingsInterface {
manual?: boolean;
querySelectors?: string;
index?: number; // previously: useRelativeAncestor + videoAncestor
mode?: 'index' | 'qs';
}
export default SettingsInterface;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,8 @@
import Debug from '../conf/Debug';
import Debug from '../../ext/conf/Debug';
class ObjectCopy {
static addNew(current, newValues){
// clone target
let out = JSON.parse(JSON.stringify(newValues));
@ -20,7 +20,7 @@ class ObjectCopy {
// Types and constructors of objects must match. If they don't, we always use the new value.
if(typeof out[k] === typeof current[k] && out[k].constructor === current[k].constructor) {
// objects are special, we need to check them recursively.
if(out[k] && typeof out[k] === 'object' && out[k].constructor === Object ) {
if(Debug.debug && Debug.settings) {
@ -29,7 +29,7 @@ class ObjectCopy {
out[k] = this.addNew(current[k], out[k]);
} else {
out[k] = current[k];
out[k] = current[k];
}
}
}
@ -52,7 +52,7 @@ class ObjectCopy {
if (current[k] !== undefined) {
// Types and constructors of objects must match. If they don't, we always use the new value.
if (typeof newValues[k] === typeof current[k] && newValues[k].constructor === current[k].constructor) {
// objects are special, we need to check them recursively.
if(current[k] && typeof current[k] === 'object' && current[k].constructor === Object ) {
if(Debug.debug && Debug.settings) {
@ -61,7 +61,7 @@ class ObjectCopy {
current[k] = this.overwrite(current[k], newValues[k]);
} else {
current[k] = newValues[k];
current[k] = newValues[k];
}
} else {
current[k] = newValues[k];
@ -82,4 +82,4 @@ class ObjectCopy {
}
}
export default ObjectCopy;
export default ObjectCopy;

14
src/common/utils/_cp.ts Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,177 +0,0 @@
<template>
<div class="uw-hover uv-hover-trigger-region">
TEST CONTENT
</div>
<div class="popup-panel">
<div class="tab-row flex flex-row">
<div class="tab">
todo: icon<br/>
Video options
</div>
</div>
<div>sudpo
<!-- Panel section -->
<template v-if="settingsInitialized">
<VideoSettings
:settings="settings"
></VideoSettings>
<ResizerDebugPanel :debugData="debugData">
</ResizerDebugPanel>
</template>
</div>
</div>
</template>
<script>
import VideoSettings from './PlayerUiPanels/VideoSettings.vue'
import { mapState } from 'vuex';
import Icon from '../common/components/Icon';
import ResizerDebugPanel from './PlayerUiPanels/ResizerDebugPanelComponent';
import BrowserDetect from '../ext/conf/BrowserDetect';
import ExecAction from './ui-libs/ExecAction';
import Logger from '../ext/lib/Logger';
import Settings from '../ext/lib/Settings';
export default {
components: {
Icon,
ResizerDebugPanel, VideoSettings
},
data() {
return {
// component properties
settings: {},
settingsInitialized: false,
execAction: new ExecAction(),
logger: null,
uiVisible: true,
debugData: {
resizer: {},
player: {},
},
debugDataPrettified: ''
};
},
computed: {
...mapState([
'showUi',
'resizerDebugData',
'playerDebugData'
]),
windowWidth: () => {
return window.innerWidth;
},
windowHeight: () => {
return window.innerHeight;
},
},
watch: {
showUi(visible) {
if (visible !== undefined) {
this.uiVisible = visible;
}
},
resizerDebugData(newData) {
this.debugData.resizer = newData;
this.debugDataPrettified = JSON.stringify(this.debugData, null, 2);
},
playerDebugData(newData) {
this.debugData.player = newData;
this.debugDataPrettified = JSON.stringify(this.debugData, null, 2);
}
},
async created() {
try {
this.logger = new Logger();
await this.logger.init({
allowLogging: true,
});
this.settings = new Settings({afterSettingsSaved: this.updateConfig, logger: this.logger});
await this.settings.init();
this.settingsInitialized = true;
this.execAction.setSettings(this.settings);
} catch (e) {
console.error('Failed to initiate ultrawidify player ui.', e);
}
},
methods: {
getUrl(url) {
return BrowserDetect.getURL(url);
},
}
}
</script>
<style lang="scss" src="../res/css/uwui-base.scss" scoped></style>
<style lang="scss" scoped>
@import '../res/css/uwui-base.scss';
@import '../res/css/colors.scss';
@import '../res/css/font/overpass.css';
@import '../res/css/font/overpass-mono.css';
@import '../res/css/common.scss';
.uw-ultrawidify-container-root {
// .relative-wrapper {
// position: relative;
// width: 100%;
// height: 100%;
// }
.uw-hover {
position: absolute;
top: 10%;
left: 10%;
width: 100px;
height: 100px;
color: #fff;
background-color: #000;
z-index: 999999999999999999;
}
.uw-hover:hover {
background-color: #f00;
}
.popup-panel {
position: absolute;
top: 10%;
left: 10%;
z-index: 999999999999999999;
width: 2500px;
height: 1200px;
max-width: 80%;
max-height: 80%;
pointer-events: all !important;
background-color: rgba(0,0,0,0.69);
color: #fff;
overflow-y: auto;
.tab {
display: block;
height: 42px;
font-size: 2.5rem;
background: rgb(87, 54, 26);
}
.tab:hover {
background-color: #f00;
}
}
pre {
white-space: pre-wrap;
}
}
</style>

View File

@ -1,134 +1,145 @@
import Debug from './conf/Debug';
import ExtensionMode from '../common/enums/ExtensionMode.enum';
import Settings from './lib/Settings';
import ActionHandler from './lib/ActionHandler';
import Comms from './lib/comms/Comms';
import CommsClient from './lib/comms/CommsClient';
import PageInfo from './lib/video-data/PageInfo';
import Logger, { baseLoggingOptions } from './lib/Logger';
import Debug from '@src/ext/conf/Debug';
import CommsClient, { CommsOrigin } from '@src/ext/module/comms/CommsClient';
import EventBus from '@src/ext/module/EventBus';
import KeyboardHandler from '@src/ext/module/kbm/KeyboardHandler';
import { ComponentLogger } from '@src/ext/module/logging/ComponentLogger';
import { BLANK_LOGGER_CONFIG, LogAggregator } from '@src/ext/module/logging/LogAggregator';
import Settings from '@src/ext/module/settings/Settings';
import { SiteSettings } from '@src/ext/module/settings/SiteSettings';
import UI from '@src/ext/module/uwui/UI';
import PageInfo from '@src/ext/module/video-data/PageInfo';
import { getIframeParentHost, setupHostnameReporting } from '@src/common/utils/getHost';
export default class UWContent {
pageInfo: PageInfo;
comms: CommsClient;
settings: Settings;
actionHandler: ActionHandler;
logger: Logger;
siteSettings: SiteSettings;
keyboardHandler: KeyboardHandler;
logAggregator: LogAggregator;
logger: ComponentLogger;
eventBus: EventBus;
isIframe: boolean = false;
parentHostname: string;
globalUi: any;
commsHandlers: {
[x: string]: ((a: any, b?: any) => void | Promise<void>)[]
} = {
'get-current-zoom': [() => this.pageInfo.requestCurrentZoom()],
'set-ar': [(message) => this.pageInfo.setAr({type: message.arg, ratio: message.customArg}, message.playing)],
'set-alignment': [(message) => {
this.pageInfo.setVideoAlignment(message.arg, message.playing);
this.pageInfo.restoreAr();
}],
'set-stretch': [(message) => this.pageInfo.setStretchMode(message.arg, message.playing, message.customArg)],
'set-keyboard': [(message) => this.pageInfo.setKeyboardShortcutsEnabled(message.arg)],
'autoar-start': [(message) => {
if (message.enabled !== false) {
this.pageInfo.initArDetection(message.playing);
this.pageInfo.startArDetection(message.playing);
} else {
this.pageInfo.stopArDetection(message.playing);
}
}],
'pause-processing': [(message) => this.pageInfo.pauseProcessing(message.playing)],
'resume-processing': [(message) => this.pageInfo.resumeProcessing(message.autoArStatus, message.playing)],
'set-zoom': [(message) => this.pageInfo.setZoom(message.arg, true, message.playing)],
'change-zoom': [(message) => this.pageInfo.zoomStep(message.arg, message.playing)],
'mark-player': [(message) => this.pageInfo.markPlayer(message.name, message.color)],
'unmark-player': [() => this.pageInfo.unmarkPlayer()],
'autoar-set-manual-tick': [(message) => this.pageInfo.setManualTick(message.arg)],
'autoar-tick': [() => this.pageInfo.tick()],
'set-ar-persistence': [(message) => this.pageInfo.setArPersistence(message.arg)],
}
constructor(){
setupHostnameReporting();
this.isIframe = window.self !== window.top;
}
reloadSettings() {
this.logger.log('info', 'debug', 'Things happened in the popup. Will reload extension settings.');
this.init();
try {
this.logger.debug('reloadSettings', 'Things happened in the popup. Will reload extension settings.');
this.init();
} catch (e) {
console.warn('Ultrawidify: settings reload failed. This probably shouldn\'t outright kill the extension, but page reload is recommended.');
}
}
async init(){
if (Debug.debug) {
console.log("[uw::main] loading configuration ...");
if (this.isIframe) {
this.parentHostname = await getIframeParentHost();
console.warn('[uw-content] got iframe parent:', this.parentHostname);
}
// logger init is the first thing that needs to run
try {
if (!this.logger) {
this.logger = new Logger();
await this.logger.init(baseLoggingOptions);
if (Debug.debug) {
console.log("[uw::main] loading configuration ...");
}
// show popup if logging to file is enabled
if (this.logger.isLoggingAllowed() && this.logger.isLoggingToFile()) {
console.info("[uw::init] Logging is allowed! Initalizing vue and UI!");
// CommsClient is not initiated yet, so we use static comms to send the command
Comms.sendMessage({cmd: 'show-logger'});
// logger init is the first thing that needs to run
try {
if (!this.logger) {
this.logAggregator = new LogAggregator('◈');
this.logger = new ComponentLogger(this.logAggregator, 'UWContent');
await this.logAggregator.init(BLANK_LOGGER_CONFIG);
}
} catch (e) {
console.error("logger init failed!", e)
}
// init() is re-run any time settings change
if (this.comms) {
this.comms.destroy();
}
if (this.eventBus) {
this.eventBus.destroy();
}
if (!this.settings) {
this.settings = new Settings({
onSettingsChanged: () => this.reloadSettings(),
logAggregator: this.logAggregator
});
await this.settings.init();
this.siteSettings = this.settings.getSiteSettings({site: window.location.hostname, isIframe: this.isIframe, parentHostname: this.parentHostname});
}
this.eventBus = new EventBus({name: 'content-script', commsOrigin: CommsOrigin.ContentScript});
this.eventBus.subscribe(
'uw-restart',
{
source: this,
function: () => this.initPhase2()
}
);
this.comms = new CommsClient('content-main-port', this.logAggregator, this.eventBus);
this.eventBus.setComms(this.comms);
this.initPhase2();
} catch (e) {
console.error("logger init failed!", e)
console.error('Ultrawidify initialization failed for some reason:', e);
}
}
// init() is re-run any time settings change
if (this.comms) {
this.comms.destroy();
}
if (!this.settings) {
this.settings = new Settings({
onSettingsChanged: () => this.reloadSettings(),
logger: this.logger
});
await this.settings.init();
}
this.comms = new CommsClient('content-main-port', this.logger, this.commsHandlers);
// če smo razširitev onemogočili v nastavitvah, ne naredimo ničesar
// If extension is soft-disabled, don't do shit
var extensionMode = this.settings.getExtensionMode();
this.logger.log('info', 'debug', "[uw::init] Extension mode:" + (extensionMode < 0 ? "disabled" : extensionMode == '1' ? 'basic' : 'full'));
const isSiteDisabled = extensionMode === ExtensionMode.Disabled
if (isSiteDisabled) {
if (this.settings.getExtensionMode('@global') === ExtensionMode.Disabled) {
this.logger.log('info', 'debug', "[uw::init] EXTENSION DISABLED, THEREFORE WONT BE STARTED")
return;
}
}
// we always initialize extension, even if it's disabled.
initPhase2() {
try {
if (this.pageInfo) {
this.logger.log('info', 'debug', '[uw.js::setup] An instance of pageInfo already exists and will be destroyed.');
this.logger.info('setup', 'An instance of pageInfo already exists and will be destroyed.');
this.pageInfo.destroy();
}
this.pageInfo = new PageInfo(this.comms, this.settings, this.logger, extensionMode, isSiteDisabled);
this.logger.log('info', 'debug', "[uw.js::setup] pageInfo initialized.");
this.logger.log('info', 'debug', "[uw.js::setup] will try to initate ActionHandler.");
this.pageInfo = new PageInfo(this.eventBus, this.siteSettings, this.settings, this.logAggregator);
this.logger.debug('setup', "pageInfo initialized.");
// start action handler only if extension is enabled for this site
if (!isSiteDisabled) {
if (this.actionHandler) {
this.actionHandler.destroy();
}
this.actionHandler = new ActionHandler(this.pageInfo);
this.actionHandler.init();
this.logger.log('info', 'debug', "[uw.js::setup] ActionHandler initiated.");
this.logger.debug('setup', "will try to initiate KeyboardHandler.");
if (this.keyboardHandler) {
this.keyboardHandler.destroy();
}
this.keyboardHandler = new KeyboardHandler(this.eventBus, this.siteSettings, this.settings, this.logAggregator);
this.keyboardHandler.init();
this.logger.debug('setup', "KeyboardHandler initiated.");
if (this.globalUi) {
this.globalUi.destroy();
}
// this.globalUi = new UI('ultrawidify-global-ui', {eventBus: this.eventBus, isGlobal: true});
// this.globalUi.enable();
// this.globalUi.setUiVisibility(false);
} catch (e) {
this.logger.log('error', 'debug', "[uw::init] FAILED TO START EXTENSION. Error:", e);
console.error('Ultrawidify: failed to start extension. Error:', e)
this.logger.error('setup', "FAILED TO START EXTENSION. Error:", e);
}
}
destroy() {
this.eventBus.unsubscribeAll(this);
if (this.pageInfo) {
this.pageInfo.destroy();
}
if (this.keyboardHandler) {
this.keyboardHandler.destroy();
}
}
}

View File

@ -1,17 +1,25 @@
import Debug from './conf/Debug.js';
import BrowserDetect from './conf/BrowserDetect';
import CommsServer from './lib/comms/CommsServer';
import Settings from './lib/Settings';
import Logger, { baseLoggingOptions } from './lib/Logger';
import { Runtime } from 'chrome';
import EventBus from '@src/ext/module/EventBus';
import Settings from '@src/ext/module/settings/Settings';
import { ComponentLogger } from '@src/ext/module/logging/ComponentLogger';
import { BLANK_LOGGER_CONFIG, LogAggregator } from '@src/ext/module/logging/LogAggregator';
import CommsServer from '@src/ext/module/comms/CommsServer';
import BrowserDetect from '@src/ext/conf/BrowserDetect';
import { HostInfo } from '@src/common/interfaces/HostData.interface';
import { ExtensionEnvironment } from '@src/common/interfaces/SettingsInterface';
import { CommsOrigin } from '@src/ext/module/comms/CommsClient';
import { sleep } from '../common/js/utils';
import { browser } from 'webextension-polyfill-ts';
const BASE_LOGGING_STYLES = {
'log': 'background-color: #243; color: #4a8',
}
export default class UWServer {
settings: Settings;
logger: Logger;
logger: ComponentLogger;
logAggregator: LogAggregator;
comms: CommsServer;
eventBus: EventBus;
ports: any[] = [];
hasVideos: boolean;
@ -24,93 +32,155 @@ export default class UWServer {
'videoSettings': undefined,
}
eventBusCommands = {
'popup-set-selected-tab': {
function: (message) => this.setSelectedTab(message.selectedMenu, message.selectedSubitem)
},
'has-video': {
function: (message, context) => this.registerVideo(context.comms.sender)
},
'noVideo' : {
function: (message, context) => this.unregisterVideo(context.comms.sender)
},
'inject-css': {
function: (message, context) => this.injectCss(message.cssString, context.comms.sender)
},
'eject-css': {
function: (message, context) => this.removeCss(message.cssString, context.comms.sender)
},
'replace-css': {
function: (message, context) => this.replaceCss(message.oldCssString, message.newCssString, context.comms.sender)
},
'get-current-site': {
function: (message, context) => this.getCurrentSite(context.comms.sender)
}
};
private gcTimeout: any;
uiLoggerInitialized: boolean = false;
//#region getters
get activeTab() {
return chrome.tabs.query({currentWindow: true, active: true});
}
//#endregion
constructor() {
this.setup();
}
async setup() {
// logger is the first thing that goes up
try {
// logger is the first thing that goes up
const loggingOptions = BLANK_LOGGER_CONFIG;
const loggingOptions = {
isBackgroundScript: true,
allowLogging: false,
useConfFromStorage: true,
logAll: true,
fileOptions: {
enabled: false,
},
consoleOptions: {
enabled: false
}
};
this.logger = new Logger();
await this.logger.init(loggingOptions);
this.logAggregator = new LogAggregator('🔶bg-script🔶');
this.logger = new ComponentLogger(this.logAggregator, 'UwServer', {styles: BASE_LOGGING_STYLES});
await this.logAggregator.init(loggingOptions);
this.settings = new Settings({logger: this.logger});
await this.settings.init();
this.comms = new CommsServer(this);
this.comms.subscribe('show-logger', async () => await this.initUiAndShowLogger());
this.comms.subscribe('init-vue', async () => await this.initUi());
this.comms.subscribe('uwui-vue-initialized', () => this.uiLoggerInitialized = true);
this.comms.subscribe('emit-logs', () => {}); // we don't need to do anything, this gets forwarded to UI content script as is
this.settings = new Settings({logAggregator: this.logAggregator});
await this.settings.init();
browser.tabs.onActivated.addListener((m) => {this.onTabSwitched(m)});
this.eventBus = new EventBus({isUWServer: true, commsOrigin: CommsOrigin.Server});
this.eventBus.subscribeMulti(this.eventBusCommands, this);
this.comms = new CommsServer(this);
this.eventBus.setComms(this.comms);
chrome.tabs.onActivated.addListener((m) => {this.onTabSwitched(m)});
} catch (e) {
console.error(`Ultrawidify [server]: failed to start. Reason:`, e);
}
}
async _promisifyTabsGet(browserObj, tabId){
return new Promise( (resolve, reject) => {
browserObj.tabs.get(tabId, (tab) => resolve(tab));
});
}
//#region CSS management
async injectCss(css, sender) {
if (!sender?.tab?.id) {
// console.warn('invalid injectCss received!');
return;
}
this.logger.info('injectCss', 'Trying to inject CSS into tab', sender.tab.id, ', frameId:', sender.frameId, 'css:\n', css)
if (!css) {
return;
}
try {
if (BrowserDetect.firefox || BrowserDetect.edge) {
browser.tabs.insertCSS(sender.tab.id, {code: css, cssOrigin: 'user', frameId: sender.frameId});
} else if (BrowserDetect.anyChromium) {
chrome.tabs.insertCSS(sender.tab.id, {code: css, cssOrigin: 'user', frameId: sender.frameId});
}
await chrome.scripting.insertCSS({
target: {
tabId: sender.tab.id,
frameIds: [
sender.frameId
]
},
css,
origin: "USER"
});
} catch (e) {
this.logger.log('error','debug', '[UwServer::injectCss] Error while injecting css:', {error: e, css, sender});
this.logger.error('injectCss', 'Error while injecting css:', {error: e, css, sender});
}
}
async removeCss(css, sender) {
try {
browser.tabs.removeCSS(sender.tab.id, {code: css, cssOrigin: 'user', frameId: sender.frameId});
} catch (e) {
this.logger.log('error','debug', '[UwServer::injectCss] Error while removing css:', {error: e, css, sender});
if (!sender?.tab) {
// console.warn('invalid removeCss received!');
return;
}
}
async replaceCss(oldCss, newCss, sender) {
if (oldCss !== newCss) {
this.injectCss(newCss, sender);
this.removeCss(oldCss, sender);
try {
await chrome.scripting.removeCSS({
target: {
tabId: sender.tab.id,
frameIds: [
sender.frameId
]
},
css,
origin: "USER"
});
} catch (e) {
this.logger.error('injectCss', 'Error while removing css:', {error: e, css, sender});
}
}
async replaceCss(oldCss, newCss, sender) {
if (!sender?.tab) {
// console.warn('invalid replaceCss received!');
return;
}
if (oldCss !== newCss) {
this.removeCss(oldCss, sender);
this.injectCss(newCss, sender);
}
}
//#endregion
extractHostname(url){
var hostname;
if (!url) {
return "<no url>";
}
// extract hostname
// 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;
}
@ -122,32 +192,36 @@ export default class UWServer {
let tab;
if (BrowserDetect.firefox) {
tab = await browser.tabs.get(this.currentTabId);
tab = await chrome.tabs.get(this.currentTabId);
} else if (BrowserDetect.anyChromium) {
tab = await this._promisifyTabsGet(chrome, this.currentTabId);
}
this.currentSite = this.extractHostname(tab.url);
this.logger.log('info', 'debug', '[UwServer::onTabSwitched] user switched tab. New site:', this.currentSite);
this.logger.info('onTabSwitched', 'user switched tab. New site:', this.currentSite);
} catch(e) {
this.logger.log('error', 'debug', '[UwServer::onTabSwitched] there was a problem getting currnet site:', e)
this.logger.info('onTabSwitched', 'there was a problem getting current site:', e)
}
this.selectedSubitem = {
'siteSettings': undefined,
'videoSettings': undefined,
}
//TODO: change extension icon based on whether there's any videos on current page
}
registerVideo(sender) {
this.logger.log('info', 'comms', '[UWServer::registerVideo] Registering video.\nsender:', sender);
if (!sender?.tab?.url) {
// console.warn('invalid registerVideo received!');
return;
}
this.logger.info('registerVideo', 'Registering video.\nsender:', sender);
const tabHostname = this.extractHostname(sender.tab.url);
const frameHostname = this.extractHostname(sender.url);
// preveri za osirotele/zastarele vrednosti ter jih po potrebi izbriši
// check for orphaned/outdated values and remove them if neccessary
// check for orphaned/outdated values and remove them if necessary
if (this.videoTabs[sender.tab.id]?.host != tabHostname) {
delete this.videoTabs[sender.tab.id]
} else if(this.videoTabs[sender.tab.id]?.frames[sender.frameId]?.host != frameHostname) {
@ -176,11 +250,16 @@ export default class UWServer {
}
}
this.logger.log('info', 'comms', '[UWServer::registerVideo] Video registered. current videoTabs:', this.videoTabs);
this.logger.info('registerVideo', 'Video registered. current videoTabs:', this.videoTabs);
}
unregisterVideo(sender) {
this.logger.log('info', 'comms', '[UwServer::unregisterVideo] Unregistering video.\nsender:', sender);
if (!sender?.tab) {
// console.warn('invalid unregisterVideo received!');
return;
}
this.logger.info('unregisterVideo', 'Unregistering video.\nsender:', sender);
if (this.videoTabs[sender.tab.id]) {
if ( Object.keys(this.videoTabs[sender.tab.id].frames).length <= 1) {
delete this.videoTabs[sender.tab.id]
@ -190,70 +269,82 @@ export default class UWServer {
}
}
}
this.logger.log('info', 'comms', '[UwServer::unregisterVideo] Video has been unregistered. Current videoTabs:', this.videoTabs);
this.logger.info('unregisterVideo', 'Video has been unregistered. Current videoTabs:', this.videoTabs);
}
setSelectedTab(menu, subitem) {
this.logger.log('info', 'comms', '[UwServer::setSelectedTab] saving selected tab for', menu, ':', subitem);
this.logger.info('setSelectedTab', 'saving selected tab for', menu, ':', subitem);
this.selectedSubitem[menu] = subitem;
}
async initUi() {
try {
if (BrowserDetect.firefox) {
await browser.tabs.executeScript({
file: '/ext/uw-ui.js',
allFrames: true,
});
} else if (BrowserDetect.anyChromium) {
await new Promise<void>( resolve =>
chrome.tabs.executeScript({
file: '/ext/uw-ui.js',
allFrames: true,
}, () => resolve())
);
}
} catch (e) {
this.logger.log('ERROR', 'uwbg', 'UI initialization failed. Reason:', e);
async getCurrentSite(sender: Runtime.MessageSender) {
// if (!sender?.tab) {
// console.warn('invalid unregisterVideo received!');
// return;
// }
const site = await this.getVideoTab();
// Don't propagate 'INVALID SITE' to the popup.
if (site.host === 'INVALID SITE') {
this.logger.info('getCurrentSite', 'Host is not valid — no info for current tab.');
return;
}
}
async initUiAndShowLogger() {
// this implementation is less than optimal and very hacky, but it should work
// just fine for our use case.
this.uiLoggerInitialized = false;
const tabHostname = await this.getCurrentTabHostname();
this.logger.info('getCurrentSite', 'Returning data:', {site, tabHostname});
console.info('get-current-site : returning data:', {site, tabHostname});
await this.initUi();
await new Promise<void>( async (resolve, reject) => {
// if content script doesn't give us a response within 5 seconds, something is
// obviously wrong and we stop waiting,
// oh and btw, resolve/reject do not break the loops, so we need to do that
// ourselves:
// https://stackoverflow.com/questions/55207256/will-resolve-in-promise-loop-break-loop-iteration
let isRejected = false;
setTimeout( async () => {isRejected = true; reject()}, 5000);
// check whether UI has been initiated on the FE. If it was, we resolve the
// promise and off we go
while (!isRejected) {
if (this.uiLoggerInitialized) {
resolve();
return; // remember the bit about resolve() not breaking the loop?
this.eventBus.send(
'set-current-site',
{
site,
tabHostname,
},
{
comms: {
forwardTo: 'popup'
}
await sleep(100);
}
})
)
}
async getCurrentTab() {
return (await browser.tabs.query({active: true, currentWindow: true}))[0];
return (await chrome.tabs.query({active: true, currentWindow: true}))[0];
}
private populateFrameVideoStatus(tabId: number, hostnames: string[]) {
const out: HostInfo[] = [];
for (const host of hostnames) {
let video = {hasVideo: false, minEnvironment: ExtensionEnvironment.Normal, maxEnvironment: ExtensionEnvironment.Fullscreen};
for (const frameKey in this.videoTabs[tabId].frames) {
const frame = this.videoTabs[tabId].frames[frameKey];
if (frame.host === host) {
video.hasVideo = true;
if (frame.environment > video.maxEnvironment) {
video.maxEnvironment = frame.environment;
}
if (frame.environment < video.minEnvironment) {
video.minEnvironment = frame.environment;
}
}
}
out.push({
host,
...video,
})
}
return out;
}
private _lastVideoTabData: any | undefined;
async getVideoTab() {
// friendly reminder: if current tab doesn't have a video,
// friendly reminder: if current tab doesn't have a video,
// there won't be anything in this.videoTabs[this.currentTabId]
const ctab = await this.getCurrentTab();
@ -262,46 +353,85 @@ export default class UWServer {
return {
host: 'INVALID SITE',
frames: [],
hostnames: [],
}
}
if (this.videoTabs[ctab.id]) {
// if video is older than PageInfo's video rescan period (+ 4000ms of grace),
// we clean it up from videoTabs[tabId].frames array.
const ageLimit = Date.now() - this.settings.active.pageInfo.timeouts.rescan - 4000;
try {
for (const key in this.videoTabs[ctab.id].frames) {
if (this.videoTabs[ctab.id].frames[key].registerTime < ageLimit) {
delete this.videoTabs[ctab.id].frames[key];
}
}
} catch (e) {
// something went wrong. There's prolly no frames.
return {
host: this.extractHostname(ctab.url),
frames: [],
selected: this.selectedSubitem
const hostnames = await this.comms.listUniqueFrameHosts();
// this probably means we're inside a problematic page
if (!this.videoTabs[ctab.id]) {
return this._lastVideoTabData ?? {
host: 'INVALID SITE',
frames: [],
hostnames: [],
}
}
// if video is older than PageInfo's video rescan period (+ 4000ms of grace),
// we clean it up from videoTabs[tabId].frames array.
const ageLimit = Date.now() - this.settings.active.pageInfo.timeouts.rescan - 4000;
try {
for (const key in this.videoTabs[ctab.id].frames) {
if (this.videoTabs[ctab.id].frames[key].registerTime < ageLimit) {
delete this.videoTabs[ctab.id].frames[key];
}
}
} catch (e) {
// something went wrong. There's prolly no frames.
// this probably shouldn't ever run.
return {
...this.videoTabs[ctab.id],
host: this.extractHostname(ctab.url),
selected: this.selectedSubitem
};
hostnames: [],
frames: [],
selected: this.selectedSubitem
}
}
// return something more or less empty if this tab doesn't have
// a video registered for it
return {
// Ensure hostnames with videos come before hostnames without videos
// Also ensure hostnames are sorted alphabetically
const populatedHostnames = this.populateFrameVideoStatus(ctab.id, hostnames);
populatedHostnames.sort((a: HostInfo, b: HostInfo) => {
return a.hasVideo === b.hasVideo
? a.host === b.host ? 0 : a.host < b.host ? -1 : 1
: a.hasVideo < b.hasVideo ? 1 : -1;
});
this._lastVideoTabData = {
host: this.extractHostname(ctab.url),
frames: [],
hostnames: populatedHostnames.map(x => x.host), // todo: try eliminating this
populatedHostnames: populatedHostnames,
selected: this.selectedSubitem
}
};
return this._lastVideoTabData;
}
// chrome shitiness mitigation
sendUnmarkPlayer(message) {
this.comms.sendUnmarkPlayer(message);
async getCurrentTabHostname() {
const activeTab = await this.activeTab;
if (!activeTab || activeTab.length < 1) {
return null;
}
const url = activeTab[0].url;
if (!url) {
console.log('no URL for active tab:', activeTab[0].url);
}
var hostname;
if (url.indexOf("://") > -1) { //find & remove protocol (http, ftp, etc.) and get hostname
hostname = url.split('/')[2];
}
else {
hostname = url.split('/')[0];
}
hostname = hostname.split(':')[0]; //find & remove port number
hostname = hostname.split('?')[0]; //find & remove "?"
return hostname;
}
}

View File

@ -179,7 +179,7 @@ var ActionList = {
page: true,
}
},
'set-ExtensionMode': {
'set-extension-mode': {
name: 'Set extension mode',
args: [{
name: 'Enable',
@ -259,4 +259,4 @@ var ActionList = {
}
};
export default ActionList;
export default ActionList;

View File

@ -5,13 +5,21 @@ if (process.env.CHANNEL !== 'stable') {
}
function detectEdgeUA() {
return /Edg\/(\.?[0-9]*)*$/.test(window.navigator.userAgent);
try {
return /Edg\/(\.?[0-9]*)*$/.test(window.navigator.userAgent);
} catch {
return undefined;
}
}
function getBrowserObj() {
return process.env.BROWSER === 'firefox' ? browser : chrome;
}
function getRuntime() {
return process.env.BROWSER === 'firefox' ? browser.runtime : chrome.runtime;
}
function getURL(url) {
return process.env.BROWSER === 'firefox' ? browser.runtime.getURL(url) : chrome.runtime.getURL(url);
}
@ -25,11 +33,12 @@ const BrowserDetect = {
processEnvChannel: process.env.CHANNEL,
isEdgeUA: detectEdgeUA(),
browserObj: getBrowserObj(),
runtime: getRuntime(),
getURL: (url) => getURL(url),
}
}
if (process.env.CHANNEL !== 'stable') {
console.info("BrowserDetect loaded:\n\nprocess.env.BROWSER:", process.env.BROWSER, "\nExporting BrowserDetect:", BrowserDetect);
}
export default BrowserDetect;
export default BrowserDetect;

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +0,0 @@
enum EdgeDetectPrimaryDirection {
Vertical = 0,
Horizontal = 1
};
export default EdgeDetectPrimaryDirection;

View File

@ -1,6 +0,0 @@
enum EdgeDetectQuality {
Fast = 0,
Improved = 1
};
export default EdgeDetectQuality;

View File

@ -1,6 +0,0 @@
enum EdgeStatus {
ARUnknown = 0,
ARKnown = 1,
};
export default EdgeStatus;

View File

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

View File

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

View File

@ -1,129 +0,0 @@
import BrowserDetect from '../../conf/BrowserDetect';
function url(file) {
return BrowserDetect.getURL(file);
}
export default class FontLoader {
static loadFonts() {
const fontsStyleElement = document.createElement('style');
fontsStyleElement.type = 'text/css';
fontsStyleElement.textContent = `
@font-face {
font-family: 'Overpass';
src: ${url('res/fonts/overpass-webfont/overpass-thin.woff2')} format('woff2'); /* Super Modern Browsers */
font-weight: 200;
font-style: normal;
}
@font-face {
font-family: 'Overpass';
src: ${url('res/fonts/overpass-webfont/overpass-thin-italic.woff2')} format('woff2');
font-weight: 200;
font-style: italic;
}
@font-face {
font-family: 'Overpass';
src: ${url('res/fonts/overpass-webfont/overpass-extralight.woff2')} format('woff2');
font-weight: 300;
font-style: normal;
}
@font-face {
font-family: 'Overpass';
src: ${url('res/fonts/overpass-webfont/overpass-extralight-italic.woff2')} format('woff2');
font-weight: 300;
font-style: italic;
}
@font-face {
font-family: 'Overpass';
src: ${url('res/fonts/overpass-webfont/overpass-light.woff2')} format('woff2');
font-weight: 400;
font-style: normal;
}
@font-face {
font-family: 'Overpass';
src: ${url('res/fonts/overpass-webfont/overpass-light-italic.woff2')} format('woff2');
font-weight: 400;
font-style: italic;
}
@font-face {
font-family: 'Overpass';
src: ${url('res/fonts/overpass-webfont/overpass-regular.woff2')} format('woff2');
font-weight: 500;
font-style: normal;
}
@font-face {
font-family: 'Overpass';
src: ${url('res/fonts/overpass-webfont/overpass-italic.woff2')} format('woff2');
font-weight: 500;
font-style: italic;
}
@font-face {
font-family: 'Overpass';
src: ${url('res/fonts/overpass-webfont/overpass-semibold.woff2')} format('woff2');
font-weight: 600;
font-style: normal;
}
@font-face {
font-family: 'Overpass';
src: ${url('res/fonts/overpass-webfont/overpass-semibold-italic.woff2')} format('woff2');
font-weight: 600;
font-style: italic;
}
@font-face {
font-family: 'Overpass';
src: ${url('res/fonts/overpass-webfont/overpass-bold.woff2')} format('woff2');
font-weight: 700;
font-style: normal;
}
@font-face {
font-family: 'Overpass';
src: ${url('res/fonts/overpass-webfont/overpass-bold-italic.woff2')} format('woff2');
font-weight: 700;
font-style: italic;
}
@font-face {
font-family: 'Overpass mono';
src: ${url('res/fonts/overpass-mono-webfont/overpass-mono-light.woff2')} format('woff2');
font-weight: 300;
font-style: normal;
}
@font-face {
font-family: 'Overpass mono';
src: ${url('res/fonts/overpass-mono-webfont/overpass-mono-regular.woff2')} format('woff2');
font-weight: 400;
font-style: normal;
}
@font-face {
font-family: 'Overpass mono';
src: ${url('res/fonts/overpass-mono-webfont/overpass-mono-semibold.woff2')} format('woff2');
font-weight: 500;
font-style: normal;
}
@font-face {
font-family: 'Overpass mono';
src: ${url('res/fonts/overpass-mono-webfont/overpass-mono-bold.woff2')} format('woff2');
font-weight: 600;
font-style: normal;
}
`;
document.head.appendChild(fontsStyleElement);
console.log("font loaded!")
}
}

View File

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

View File

@ -1,102 +0,0 @@
import UI from './UI';
import VuexWebExtensions from 'vuex-webextensions';
import PlayerUiComponent from '../../../csui/PlayerUiComponent.vue';
if (process.env.CHANNEL !== 'stable'){
console.info("Loading: PlayerUi");
}
/**
* Class that handles in-player UI
*/
class PlayerUi extends UI {
/**
* Creates new in-player UI for ultrawidify
* @param {*} playerElement PlayerUI will be created as a child of this element
* @param {*} settings Extension settings (instanceof Settings)
*/
constructor (
playerElement,
settings
) {
super(
'ultrawidifyUi',
PlayerUi.getStoreConfig(),
PlayerUi.getUiConfig(playerElement),
PlayerUi.getCommsConfig()
);
this.settings = settings;
}
//#region constructor helpers
// we will move some things out of the constructor in order to keep things clean
static getStoreConfig() {
// NOTE: these are sample values and can be deleted. Currently, they're commented out
// so we won't have to look up the documentation in order to get them working
return {
plugins: [
VuexWebExtensions({
persistentStates: [
'showUi'
],
}),
],
state: {
showUi: true,
},
mutations: {
'uw-toggle-ui'(state) {
state['showUi'] = !state['showUi'];
},
'uw-set-ui-visible'(state, payload) {
state['showUi'] = payload;
}
},
actions: {
'uw-set-ui-visible'({commit}, payload) {
commit('uw-set-ui-visible', payload);
},
'uw-toggle-ui'({commit}) {
commit('uw-toggle-ui');
}
}
};
}
static getUiConfig(playerElement) {
return {
parentElement: playerElement,
component: PlayerUiComponent
}
}
static getCommsConfig() {
// NOTE: these are sample values and can be deleted. Currently, they're commented out
// so we won't have to look up the documentation in order to get them working
return {
handlers: {
// 'show-notification': [(message) => this.showNotification(message)],
}
}
}
//#endregion
//#region lifecycle
replace(playerElement) {
try {
super.replace(this.getUiConfig(playerElement));
} catch (e) {
this.logger.log('error', 'Couldn\'t replace player element for ui. Error:', e);
}
}
//#endregion
}
if (process.env.CHANNEL !== 'stable'){
console.info("PlayerUi loaded");
}
export default PlayerUi;

View File

@ -1,87 +0,0 @@
import { createApp } from 'vue';
import { createStore } from 'vuex';
if (process.env.CHANNEL !== 'stable'){
console.info("Loading: UI");
}
class UI {
constructor(
interfaceId,
storeConfig,
uiConfig, // {component, parentElement?}
commsConfig,
) {
this.interfaceId = interfaceId;
this.commsConfig = commsConfig;
this.storeConfig = storeConfig;
this.uiConfig = uiConfig;
this.init();
}
async init() {
// initialize vuejs, but only once (check handled in initVue())
// we need to initialize this _after_ initializing comms.
this.initVue();
}
async initVue() {
if (this.storeConfig) {
this.vuexStore = createStore(this.storeConfig);
}
this.initUi();
}
async initUi() {
const random = Math.round(Math.random() * 69420);
const uwid = `uw-${this.interfaceId}-root-${random}`
const rootDiv = document.createElement('div');
if (this.uiConfig.additionalStyle) {
rootDiv.setAttribute('style', this.uiConfig.additionalStyle);
}
rootDiv.setAttribute('id', uwid);
rootDiv.classList.add('uw-ultrawidify-container-root');
if (this.uiConfig?.parentElement) {
this.uiConfig.parentElement.appendChild(rootDiv);
} else {
document.body.appendChild(rootDiv);
}
this.element = rootDiv;
const app = createApp(this.uiConfig.component);
if (this.vuexStore) {
app.use(this.vuexStore);
}
app.mount(`#${uwid}`);
}
/**
* Replaces ui config and re-inits the UI
* @param {*} newUiConfig
*/
replace(newUiConfig) {
this.element?.remove();
this.uiConfig = newUiConfig;
this.initUi();
}
destroy() {
// this.comms?.destroy();
this.element?.remove();
}
}
if (process.env.CHANNEL !== 'stable'){
console.info("UI.js loaded");
}
export default UI;

View File

@ -1,658 +0,0 @@
import Debug from '../../conf/Debug';
import VideoData from './VideoData';
import RescanReason from './enums/RescanReason.enum';
import AspectRatioType from '../../../common/enums/AspectRatioType.enum';
import CropModePersistence from '../../../common/enums/CropModePersistence.enum';
import Logger from '../Logger';
import Settings from '../Settings';
import ExtensionMode from '../../../common/enums/ExtensionMode.enum';
import CommsClient from '../comms/CommsClient';
if (process.env.CHANNEL !== 'stable'){
console.info("Loading PageInfo");
}
class PageInfo {
//#region flags
readOnly: boolean = false;
hasVideos: boolean = false;
siteDisabled: boolean = false;
//#endregion
//#region timers and timeouts
rescanTimer: any;
urlCheckTimer: any;
announceZoomTimeout: any;
//#endregion
//#region helper objects
logger: Logger;
settings: Settings;
comms: CommsClient;
videos: VideoData[] = [];
//#endregion
//#region misc stuff
lastUrl: string;
extensionMode: ExtensionMode;
defaultCrop: any;
currentCrop: any;
actionHandlerInitQueue: any[] = [];
currentZoomScale: number = 1;
actionHandler: any;
//#endregion
constructor(comms, settings, logger, extensionMode, readOnly = false){
this.logger = logger;
this.settings = settings;
this.lastUrl = window.location.href;
this.extensionMode = extensionMode;
this.readOnly = readOnly;
if (comms){
this.comms = comms;
}
try {
// request inject css immediately
const playerStyleString = this.settings.active.sites[window.location.hostname].css.replace('\\n', '');
this.comms.sendMessage({
cmd: 'inject-css',
cssString: playerStyleString
});
} catch (e) {
// do nothing. It's ok if there's no special settings for the player element or crop persistence
}
// try getting default crop immediately.
// const cropModePersistence = this.settings.getDefaultCropPersistenceMode(window.location.hostname);
// try {
// if (cropModePersistence === CropModePersistence.Forever) {
// this.defaultCrop = this.settings.active.sites[window.location.hostname].defaultCrop;
// } else if (cropModePersistence === CropModePersistence.CurrentSession) {
// this.defaultCrop = JSON.parse(sessionStorage.getItem('uw-crop-mode-session-persistence'));
// }
// } catch (e) {
// // do nothing. It's ok if there's no special settings for the player element or crop persistence
// }
this.currentCrop = this.defaultCrop;
this.rescan(RescanReason.PERIODIC);
this.scheduleUrlCheck();
}
async injectCss(cssString) {
await this.comms.sendMessage({
cmd: 'inject-css',
cssString: cssString
});
}
async ejectCss(cssString) {
await this.comms.sendMessage({
cmd: 'eject-css',
cssString: cssString
});
}
async replaceCss(oldCssString, newCssString) {
await this.comms.sendMessage({
cmd: 'replace-css',
newCssString,
oldCssString
});
}
destroy() {
this.logger.log('info', ['debug', 'init'], "[PageInfo::destroy] destroying all videos!")
if(this.rescanTimer){
clearTimeout(this.rescanTimer);
}
for (let video of this.videos) {
try {
(this.comms.unregisterVideo as any)(video.vdid)
video.destroy();
} catch (e) {
this.logger.log('error', ['debug', 'init'], '[PageInfo::destroy] unable to destroy video! Error:', e);
}
}
try {
const playerStyleString = this.settings.active.sites[window.location.hostname].css;
if (playerStyleString) {
this.comms.sendMessage({
cmd: 'eject-css',
cssString: playerStyleString
});
}
} catch (e) {
// do nothing. It's ok if there's no special settings for the player element
}
}
reset() {
for(let video of this.videos) {
video.destroy();
}
this.rescan(RescanReason.MANUAL);
}
initMouseActionHandler(videoData) {
if (this.actionHandler) {
this.actionHandler.registerHandleMouse(videoData);
} else {
this.actionHandlerInitQueue.push(videoData);
}
}
setActionHandler(actionHandler) {
this.actionHandler = actionHandler;
for (let item of this.actionHandlerInitQueue) {
this.actionHandler.registerHandleMouse(item);
}
this.actionHandlerInitQueue = [];
}
getVideos(host) {
if (this.settings.active.sites[host]?.DOM?.video?.manual
&& this.settings.active.sites[host]?.DOM?.video?.querySelectors){
const videos = document.querySelectorAll(this.settings.active.sites[host].DOM.video.querySelectors);
if (videos.length) {
return videos;
}
}
return document.getElementsByTagName('video');
}
hasVideo() {
return this.readOnly ? this.hasVideos : this.videos.length;
}
rescan(rescanReason){
const oldVideoCount = this.videos.length;
try{
let vids = this.getVideos(window.location.hostname);
if(!vids || vids.length == 0){
this.hasVideos = false;
if(rescanReason == RescanReason.PERIODIC){
this.logger.log('info', 'videoRescan', "[PageInfo::rescan] Scheduling normal rescan.")
this.scheduleRescan(RescanReason.PERIODIC);
}
return;
}
// add new videos
this.hasVideos = false;
let videoExists = false;
let video, v;
for (video of vids) {
// če najdemo samo en video z višino in širino, to pomeni, da imamo na strani veljavne videe
// če trenutni video nima definiranih teh vrednostih, preskočimo vse nadaljnja preverjanja
// <===[:::::::]===>
// if we find even a single video with width and height, that means the page has valid videos
// if video lacks either of the two properties, we skip all further checks cos pointless
if(video.offsetWidth && video.offsetHeight){
this.hasVideos = true;
if (this.readOnly) {
// in lite mode, we're done. This is all the info we want, but we want to actually start doing
// things that interfere with the website. We still want to be running a rescan, tho.
if(rescanReason == RescanReason.PERIODIC){
this.scheduleRescan(RescanReason.PERIODIC);
}
return;
}
} else {
continue;
}
videoExists = false;
for (v of this.videos) {
if (v.destroyed) {
continue; //TODO: if destroyed video is same as current video, copy aspect ratio settings to current video
}
if (v.video == video) {
videoExists = true;
break;
}
}
if (videoExists) {
continue;
} else {
this.logger.log('info', 'videoRescan', "[PageInfo::rescan] found new video candidate:", video, "NOTE:: Video initialization starts here:\n--------------------------------\n")
try {
v = new VideoData(video, this.settings, this);
this.videos.push(v);
} catch (e) {
this.logger.log('error', 'debug', "rescan error: failed to initialize videoData. Skipping this video.",e);
}
this.logger.log('info', 'videoRescan', "END VIDEO INITIALIZATION\n\n\n-------------------------------------\nvideos[] is now this:", this.videos,"\n\n\n\n\n\n\n\n")
}
}
this.removeDestroyed();
// če smo ostali brez videev, potem odregistriraj stran.
// če nismo ostali brez videev, potem registriraj stran.
//
// if we're left without videos on the current page, we unregister the page.
// if we have videos, we call register.
if (this.comms) {
// We used to send "register video" requests only on the first load, or if the number of
// videos on the page has changed. However, since Chrome Web Store started to require every
// extension requiring "broad permissions" to undergo manual review
// ... and since Chrome Web Store is known for taking their sweet ass time reviewing extensions,
// with review times north of an entire fucking month
// ... and since the legacy way of checking whether our frames-with-videos cache in background
// script contains any frames that no longer exist required us to use webNavigation.getFrame()/
// webNavigation.getAllFrames(), which requires a permission that triggers a review.
//
// While the extension uses some other permissions that trigger manual review, it's said that
// less is better / has a positive effect on your manual review times ... So I guess we'll do
// things in the less-than-optimal. more-than-retarded way.
//
// no but honestly fuck Chrome.
// if (this.videos.length != oldVideoCount) {
// }
if (this.videos.length > 0) {
// this.comms.registerVideo({host: window.location.hostname, location: window.location});
this.comms.registerVideo();
} else {
// this.comms.unregisterVideo({host: window.location.hostname, location: window.location});
this.comms.unregisterVideo();
}
}
} catch(e) {
// če pride do zajeba, potem lahko domnevamo da na strani ni nobenega videa. Uničimo vse objekte videoData
// da preprečimo večkratno inicializacijo. Če smo se z našim ugibom zmotili, potem se bodo vsi videi ponovno
// našli ob naslednjem preiskovanju
//
// if we encounter a fuckup, we can assume that no videos were found on the page. We destroy all videoData
// objects to prevent multiple initialization (which happened, but I don't know why). No biggie if we destroyed
// videoData objects in error — they'll be back in the next rescan
this.logger.log('error', 'debug', "rescan error: — destroying all videoData objects",e);
for (const v of this.videos) {
v.destroy();
}
return;
}
if(rescanReason == RescanReason.PERIODIC){
this.scheduleRescan(RescanReason.PERIODIC);
}
}
removeDestroyed(){
this.videos = this.videos.filter( vid => vid.destroyed === false);
}
scheduleRescan(rescanReason){
if(rescanReason != RescanReason.PERIODIC){
this.rescan(rescanReason);
return;
}
try{
if(this.rescanTimer){
clearTimeout(this.rescanTimer);
}
let ths = this;
this.rescanTimer = setTimeout(function(rescanReason){
ths.rescanTimer = null;
ths.rescan(rescanReason);
ths = null;
}, this.settings.active.pageInfo.timeouts.rescan, RescanReason.PERIODIC)
} catch(e) {
this.logger.log('error', 'debug', "[PageInfo::scheduleRescan] scheduling rescan failed. Here's why:",e)
}
}
scheduleUrlCheck() {
try{
if(this.urlCheckTimer){
clearTimeout(this.urlCheckTimer);
}
let ths = this;
this.urlCheckTimer = setTimeout(function(){
ths.urlCheckTimer = null;
ths.ghettoUrlCheck();
ths = null;
}, this.settings.active.pageInfo.timeouts.urlCheck)
} catch(e){
this.logger.log('error', 'debug', "[PageInfo::scheduleUrlCheck] scheduling URL check failed. Here's why:",e)
}
}
ghettoUrlCheck() {
if (this.lastUrl != window.location.href){
this.logger.log('error', 'videoRescan', "[PageInfo::ghettoUrlCheck] URL has changed. Triggering a rescan!");
this.rescan(RescanReason.URL_CHANGE);
this.lastUrl = window.location.href;
}
this.scheduleUrlCheck();
}
initArDetection(playingOnly){
if (playingOnly) {
for(let vd of this.videos){
if(vd.isPlaying()) {
vd.initArDetection();
}
}
return;
} else {
for(let vd of this.videos){
vd.initArDetection();
}
}
}
// to je treba klicat ob menjavi zavihkov
// these need to be called on tab switch
pauseProcessing(playingOnly){
if (playingOnly) {
for(let vd of this.videos){
if (vd.isPlaying()) {
vd.pause();
}
}
} else {
for(let vd of this.videos){
vd.pause();
}
}
}
resumeProcessing(resumeAutoar = false, playingOnly = false){
if (playingOnly) {
for(let vd of this.videos){
if (vd.isPlaying()) {
vd.resume();
if(resumeAutoar){
vd.resumeAutoAr();
}
}
}
} else {
for(let vd of this.videos){
vd.resume();
if(resumeAutoar){
vd.resumeAutoAr();
}
}
}
}
startArDetection(playingOnly){
if (Debug.debug) {
this.logger.log('info', 'debug', '[PageInfo::startArDetection()] starting automatic ar detection!')
}
if (playingOnly) {
for(let vd of this.videos){
if (vd.isPlaying()) {
vd.startArDetection();
}
}
} else {
for(let vd of this.videos){
vd.startArDetection();
}
}
}
stopArDetection(playingOnly){
if (playingOnly) {
for(let vd of this.videos){
if (vd.isPlaying()) {
vd.stopArDetection();
}
}
} else {
for(let vd of this.videos){
vd.stopArDetection();
}
}
}
setAr(ar, playingOnly?: boolean){
this.logger.log('info', 'debug', '[PageInfo::setAr] aspect ratio:', ar, "playing only?", playingOnly)
if (ar.type !== AspectRatioType.Automatic) {
this.stopArDetection(playingOnly);
} else {
this.logger.log('info', 'debug', '[PageInfo::setAr] aspect ratio is auto');
try {
for (let vd of this.videos) {
if (!playingOnly || vd.isPlaying()) {
vd.resetLastAr();
}
}
} catch (e) {
this.logger.log('error', 'debug', "???", e);
}
this.initArDetection(playingOnly);
this.startArDetection(playingOnly);
return;
}
// TODO: find a way to only change aspect ratio for one video
if (ar === AspectRatioType.Reset) {
for (let vd of this.videos) {
if (!playingOnly || vd.isPlaying()) {
vd.resetAr();
}
}
} else {
for (let vd of this.videos) {
if (!playingOnly || vd.isPlaying()) {
vd.setAr(ar)
}
}
}
}
setVideoAlignment(videoAlignment, playingOnly) {
if (playingOnly) {
for(let vd of this.videos) {
if (vd.isPlaying()) {
vd.setVideoAlignment(videoAlignment)
}
}
} else {
for(let vd of this.videos) {
vd.setVideoAlignment(videoAlignment)
}
}
}
setPanMode(mode, playingOnly?: boolean) {
if (playingOnly) {
for(let vd of this.videos) {
if (vd.isPlaying()) {
vd.setPanMode(mode);
}
}
} else {
for(let vd of this.videos) {
vd.setPanMode(mode);
}
}
}
restoreAr(playingOnly?: boolean) {
if (playingOnly) {
for(let vd of this.videos){
if (vd.isPlaying()) {
vd.restoreAr();
}
}
} else {
for(let vd of this.videos){
vd.restoreAr();
}
}
}
setStretchMode(stretchMode, playingOnly?: boolean, fixedStretchRatio?: boolean){
// TODO: find a way to only change aspect ratio for one video
if (playingOnly) {
for(let vd of this.videos){
if (vd.isPlaying()) {
vd.setStretchMode(stretchMode, fixedStretchRatio)
}
}
} else {
for(let vd of this.videos){
vd.setStretchMode(stretchMode, fixedStretchRatio)
}
}
}
setZoom(zoomLevel, no_announce?: boolean, playingOnly?: boolean) {
if (playingOnly) {
for(let vd of this.videos) {
if (vd.isPlaying()) {
vd.setZoom(zoomLevel, no_announce);
}
}
} else {
for(let vd of this.videos) {
vd.setZoom(zoomLevel, no_announce);
}
}
}
zoomStep(step, playingOnly?: boolean) {
for(let vd of this.videos){
if (!playingOnly || vd.isPlaying()) {
vd.zoomStep(step);
}
}
}
markPlayer(name, color) {
for (let vd of this.videos) {
vd.markPlayer(name,color);
}
}
unmarkPlayer() {
for (let vd of this.videos) {
vd.unmarkPlayer();
}
}
announceZoom(scale) {
if (this.announceZoomTimeout) {
clearTimeout(this.announceZoomTimeout);
}
this.currentZoomScale = scale;
const ths = this;
this.announceZoomTimeout = setTimeout(() => ths.comms.announceZoom(scale), this.settings.active.zoom.announceDebounce);
}
setManualTick(manualTick) {
for(let vd of this.videos) {
vd.setManualTick(manualTick);
}
}
tick() {
for(let vd of this.videos) {
vd.tick();
}
}
sendPerformanceUpdate(performanceUpdate) {
if(this.comms) {
this.comms.sendPerformanceUpdate(performanceUpdate);
}
}
requestCurrentZoom() {
this.comms.announceZoom(this.currentZoomScale);
}
setKeyboardShortcutsEnabled(state) {
this.actionHandler.setKeyboardLocal(state);
}
setArPersistence(persistenceMode) {
// name of this function is mildly misleading — we don't really _set_ ar persistence. (Ar persistence
// mode is set and saved via popup or keyboard shortcuts, if user defined them) We just save the current
// aspect ratio whenever aspect ratio persistence mode changes.
if (persistenceMode === CropModePersistence.CurrentSession) {
sessionStorage.setItem('uw-crop-mode-session-persistence', JSON.stringify(this.currentCrop));
} else if (persistenceMode === CropModePersistence.Forever) {
if (this.settings.active.sites[window.location.hostname]) {
// | key may be missing, so we do this
this.settings.active.sites[window.location.hostname]['defaultAr'] = this.currentCrop;
} else {
this.settings.active.sites[window.location.hostname] = this.settings.getDefaultOption();
this.settings.active.sites[window.location.hostname]['defaultAr'] = this.currentCrop;
}
this.settings.saveWithoutReload();
}
}
updateCurrentCrop(ar) {
this.currentCrop = ar;
// This means crop persistance is disabled. If crop persistance is enabled, then settings for current
// site MUST exist (crop persistence mode is disabled by default)
const cropModePersistence = this.settings.getDefaultCropPersistenceMode(window.location.hostname);
if (cropModePersistence === CropModePersistence.Disabled) {
return;
}
this.defaultCrop = ar;
if (cropModePersistence === CropModePersistence.CurrentSession) {
sessionStorage.setItem('uw-crop-mode-session-persistence', JSON.stringify(ar));
} else if (cropModePersistence === CropModePersistence.Forever) {
if (this.settings.active.sites[window.location.hostname]) {
// | key may be missing, so we do this
this.settings.active.sites[window.location.hostname]['defaultAr'] = ar;
} else {
this.settings.active.sites[window.location.hostname] = this.settings.getDefaultOption();
this.settings.active.sites[window.location.hostname]['defaultAr'] = ar;
}
this.settings.saveWithoutReload();
}
}
}
if (process.env.CHANNEL !== 'stable'){
console.info("PageInfo loaded!");
}
export default PageInfo;

View File

@ -1,548 +0,0 @@
import Debug from '../../conf/Debug';
import ExtensionMode from '../../../common/enums/ExtensionMode.enum'
import AspectRatioType from '../../../common/enums/AspectRatioType.enum';
import PlayerNotificationUi from '../uwui/PlayerNotificationUI';
import PlayerUi from '../uwui/PlayerUI';
import BrowserDetect from '../../conf/BrowserDetect';
import * as _ from 'lodash';
import { sleep } from '../../../common/js/utils';
import VideoData from './VideoData';
import Settings from '../Settings';
import Logger from '../Logger';
if (process.env.CHANNEL !== 'stable'){
console.info("Loading: PlayerData.js");
}
/* sprejme <video> tag (element) in seznam imen, ki se lahko pojavijo v razredih oz. id staršev.
// vrne dimenzije predvajalnika (širina, višina)
//
// Na youtube v theater mode je razširitev rahlo pokvarjena. Video tag ostane večji od predvajalnika, ko se zapusti
// celozaslonski način. Ta funkcija skuša to težavo rešiti tako, da poišče element predvajalnika, ki je zavit okoli videa.
//
// Funkcija izkorišča lastnost, da bi načeloma moral biti vsak zunanji element večji od notranjega. Najmanjši element od
// <video> značke pa do korena drevesa bi tako moral biti predvajalnik.
//
// Če je podan seznam imen, potem funkcija vrne dimenzije prvega elementa, ki v id oz. razredu vsebuje katerokoli ime iz seznama
//
// | EN |
//
// accepts <video> tag (element) and list of names that can appear in id or class
// returns player dimensions (width, height)
//
// Theater mode is mildly broken on youtube. <video> tag remains bigger than the player after leaving the fullscreen mode, and
// there's nothing we can do about that. This function aims to solve the problem by finding the player element that's wrapped around
// the <video> tag.
//
// In general, an outer tag should be bigger than the inner tag. Therefore the smallest element between <video> tag and the document
// root should be the player.
//
// If list of names is provided, the function returns dimensions of the first element that contains any name from the list in either
// id or class.
*/
class PlayerData {
//#region helper objects
logger: Logger;
videoData: VideoData;
settings: Settings;
notificationService: PlayerNotificationUi;
//#endregion
//#region HTML objects
video: any;
element: any;
overlayNode: any;
//#endregion
//#region flags
invalid: boolean = false;
private periodicallyRefreshPlayerElement: boolean = false;
halted: boolean = true;
//#region misc stuff
extensionMode: any;
dimensions: {width?: number, height?: number, fullscreen?: boolean};
private playerIdElement: any;
private observer: ResizeObserver;
//#endregion
constructor(videoData) {
try {
this.logger = videoData.logger;
this.videoData = videoData;
this.video = videoData.video;
this.settings = videoData.settings;
this.extensionMode = videoData.extensionMode;
this.invalid = false;
this.element = this.getPlayer();
this.notificationService = new PlayerNotificationUi(this.element, this.settings);
this.dimensions = undefined;
this.overlayNode = undefined;
this.periodicallyRefreshPlayerElement = false;
try {
this.periodicallyRefreshPlayerElement = this.settings.active.sites[window.location.hostname].DOM.player.periodicallyRefreshPlayerElement;
} catch (e) {
// no biggie — that means we don't have any special settings for this site.
}
// this happens when we don't find a matching player element
if (!this.element) {
this.invalid = true;
return;
}
if (this.extensionMode === ExtensionMode.Enabled) {
this.checkPlayerSizeChange();
}
this.startChangeDetection();
} catch (e) {
console.error('[Ultrawidify::PlayerData::ctor] There was an error setting up player data. You should be never seeing this message. Error:', e);
this.invalid = true;
}
}
static isFullScreen(){
return ( window.innerHeight == window.screen.height && window.innerWidth == window.screen.width);
}
onPlayerDimensionsChanged(mutationList?, observer?) {
if (this?.checkPlayerSizeChange()) {
this.videoData.resizer.restore();
}
}
start(){
this.startChangeDetection();
}
stop(){
this.halted = true;
this.stopChangeDetection();
}
destroy() {
this.stopChangeDetection();
this.destroyOverlay();
this.notificationService?.destroy();
}
startChangeDetection(){
if (this.invalid) {
return;
}
try {
if (BrowserDetect.firefox) {
this.observer = new ResizeObserver(
_.debounce( // don't do this too much:
this.onPlayerDimensionsChanged,
250, // do it once per this many ms
{
leading: true, // do it when we call this fallback first
trailing: true // do it after the timeout if we call this callback few more times
}
)
);
} else {
// Chrome for some reason insists that this.onPlayerDimensionsChanged is not a function
// when it's not wrapped into an anonymous function
this.observer = new ResizeObserver(
_.debounce( // don't do this too much:
(m,o) => this.onPlayerDimensionsChanged(m,o),
250, // do it once per this many ms
{
leading: true, // do it when we call this fallback first
trailing: true // do it after the timeout if we call this callback few more times
}
)
);
}
const observerConf = {
attributes: true,
// attributeFilter: ['style', 'class'],
attributeOldValue: true,
};
this.observer.observe(this.element);
} catch (e) {
console.error("failed to set observer",e )
}
// legacy mode still exists, but acts as a fallback for observers and is triggered less
// frequently in order to avoid too many pointless checks
this.legacyChangeDetection();
}
async legacyChangeDetection() {
while (!this.halted) {
await sleep(1000);
try {
this.doPeriodicPlayerElementChangeCheck();
} catch (e) {
console.error('[PlayerData::legacycd] this message is pretty high on the list of messages you shouldn\'t see', e);
}
}
}
doPeriodicPlayerElementChangeCheck() {
if (this.periodicallyRefreshPlayerElement) {
this.forceDetectPlayerElementChange();
}
}
stopChangeDetection(){
this.observer.disconnect();
}
makeOverlay() {
if (!this.overlayNode) {
this.destroyOverlay();
}
let overlay = document.createElement('div');
overlay.style.width = '100%';
overlay.style.height = '100%';
overlay.style.position = 'absolute';
overlay.style.top = '0';
overlay.style.left = '0';
overlay.style.zIndex = '1000000000';
overlay.style.pointerEvents = 'none';
this.overlayNode = overlay;
this.element.appendChild(overlay);
}
destroyOverlay() {
if(this.playerIdElement) {
this.playerIdElement.remove();
this.playerIdElement = undefined;
}
if (this.overlayNode) {
this.overlayNode.remove();
this.overlayNode = undefined;
}
}
markPlayer(name, color) {
if (!this.overlayNode) {
this.makeOverlay();
}
if (this.playerIdElement) {
this.playerIdElement.remove();
}
this.playerIdElement = document.createElement('div');
this.playerIdElement.innerHTML = `<div style="background-color: ${color}; color: #fff; position: absolute; top: 0; left: 0">${name}</div>`;
this.overlayNode.appendChild(this.playerIdElement);
}
unmarkPlayer() {
this.logger.log('info', 'debug', "[PlayerData::unmarkPlayer] unmarking player!", {playerIdElement: this.playerIdElement});
if (this.playerIdElement) {
this.playerIdElement.innerHTML = '';
this.playerIdElement.remove();
}
this.playerIdElement = undefined;
}
collectionHas(collection, element) {
for (let i = 0, len = collection.length; i < len; i++) {
if (collection[i] == element) {
return true;
}
}
return false;
}
updatePlayerDimensions(element) {
const isFullScreen = PlayerData.isFullScreen();
if (element.offsetWidth !== this.dimensions?.width
|| element.offsetHeight !== this.dimensions?.height
|| isFullScreen !== this.dimensions?.fullscreen) {
// update dimensions only if they've changed, _before_ we do a restore (not after)
this.dimensions = {
width: element.offsetWidth,
height: element.offsetHeight,
fullscreen: isFullScreen
};
// actually re-calculate zoom when player size changes, but only if videoData.resizer
// is defined. Since resizer needs a PlayerData object to exist, videoData.resizer will
// be undefined the first time this function will run.
this.videoData.resizer?.restore();
// NOTE: it's possible that notificationService hasn't been initialized yet at this point.
// no biggie if it wasn't, we just won't replace the notification UI
this.notificationService?.replace(this.element);
}
}
getPlayer() {
const host = window.location.hostname;
let element = this.video.parentNode;
const videoWidth = this.video.offsetWidth;
const videoHeight = this.video.offsetHeight;
const elementQ = [];
let scorePenalty = 0;
let score;
try {
if(! element ){
this.logger.log('info', 'debug', "[PlayerDetect::_pd_getPlayer] element is not valid, doing nothing.", element)
if(this.element) {
const ths = this;
}
this.element = undefined;
this.dimensions = undefined;
return;
}
// log the entire hierarchy from <video> to root
if (this.logger.canLog('playerDetect')) {
const logObj = [];
logObj.push(`window size: ${window.innerWidth} x ${window.innerHeight}`);
let e = element;
while (e) {
logObj.push({offsetSize: {width: e.offsetWidth, height: e.offsetHeight}, clientSize: {width: e.clientWidth, height: e.clientHeight}, element: e});
e = e.parentNode;
}
this.logger.log('info', 'playerDetect', "\n\n[PlayerDetect::getPlayer()] element hierarchy (video->root)", logObj);
}
if (this.settings.active.sites[host]?.DOM?.player?.manual) {
if (this.settings.active.sites[host]?.DOM?.player?.useRelativeAncestor
&& this.settings.active.sites[host]?.DOM?.player?.videoAncestor) {
let parentsLeft = this.settings.active.sites[host].DOM.player.videoAncestor - 1;
while (parentsLeft --> 0) {
element = element.parentNode;
}
if (element) {
this.updatePlayerDimensions(element);
return element;
}
} else if (this.settings.active.sites[host]?.DOM?.player?.querySelectors) {
const allSelectors = document.querySelectorAll(this.settings.active.sites[host].DOM.player.querySelectors);
// actually we'll also score this branch in a similar way we score the regular, auto branch
while (element) {
// Let's see how this works
if (this.collectionHas(allSelectors, element)) {
score = 100; // every matching element gets a baseline 100 points
// elements that match the size get a hefty bonus
if ( (element.offsetWidth >= videoWidth && this.equalish(element.offsetHeight, videoHeight, 2))
|| (element.offsetHeight >= videoHeight && this.equalish(element.offsetWidth, videoHeight, 2))) {
score += 75;
}
// elements farther away from the video get a penalty
score -= (scorePenalty++) * 20;
// push the element on the queue/stack:
elementQ.push({
score: score,
element: element,
});
}
element = element.parentNode;
}
// log player candidates
this.logger.log('info', 'playerDetect', 'player detect via query selector: element queue and final element:', {queue: elementQ, bestCandidate: elementQ.length ? elementQ.sort( (a,b) => b.score - a.score)[0].element : 'n/a'});
if (elementQ.length) {
// return element with biggest score
// if video player has not been found, proceed to automatic detection
const playerElement = elementQ.sort( (a,b) => b.score - a.score)[0].element;
this.updatePlayerDimensions(playerElement);
return playerElement;
}
}
}
// try to find element the old fashioned way
while (element){
// odstranimo čudne elemente, ti bi pokvarili zadeve
// remove weird elements, those would break our stuff
if ( element.offsetWidth == 0 || element.offsetHeight == 0){
element = element.parentNode;
continue;
}
// element je player, če je ena stranica enako velika kot video, druga pa večja ali enaka.
// za enakost dovolimo mala odstopanja
// element is player, if one of the sides is as long as the video and the other bigger (or same)
// we allow for tiny variations when checking for equality
if ( (element.offsetWidth >= videoWidth && this.equalish(element.offsetHeight, videoHeight, 2))
|| (element.offsetHeight >= videoHeight && this.equalish(element.offsetWidth, videoHeight, 2))) {
// todo — in case the match is only equalish and not exact, take difference into account when
// calculating score
score = 100;
// This entire section is disabled because of some bullshit on vk and some shady CIS streaming sites.
// Possibly removal of this criteria is not necessary, because there was also a bug with force player
//
// if (element.id.indexOf('player') !== -1) { // prefer elements with 'player' in id
// score += 75;
// }
// this has only been observed on steam
// if (element.id.indexOf('movie') !== -1) {
// score += 75;
// }
// if (element.classList.toString().indexOf('player') !== -1) { // prefer elements with 'player' in classlist, but a bit less than id
// score += 50;
// }
score -= scorePenalty++; // prefer elements closer to <video>
elementQ.push({
element: element,
score: score,
});
}
element = element.parentNode;
}
// log player candidates
this.logger.log('info', 'playerDetect', 'player detect, auto/fallback: element queue and final element:', {queue: elementQ, bestCandidate: elementQ.length ? elementQ.sort( (a,b) => b.score - a.score)[0].element : 'n/a'});
if (elementQ.length) {
// return element with biggest score
const playerElement = elementQ.sort( (a,b) => b.score - a.score)[0].element;
this.updatePlayerDimensions(playerElement);
return playerElement;
}
// if no candidates were found, something is obviously very, _very_ wrong.
// we return nothing. Player will be marked as invalid and setup will stop.
// VideoData should check for that before starting anything.
this.logger.log('warn', 'debug', '[PlayerData::getPlayer] no matching player was found for video', this.video, 'Extension cannot work on this site.');
return;
} catch (e) {
this.logger.log('crit', 'debug', '[PlayerData::getPlayer] something went wrong while detecting player:', e, 'Shutting down extension for this page');
}
}
equalish(a,b, tolerance) {
return a > b - tolerance && a < b + tolerance;
}
forceDetectPlayerElementChange() {
// Player dimension changes get calculated every time updatePlayerDimensions is called (which happens
// every time getPlayer() detects an element). If updatePlayerDimension detects dimensions were changed,
// it will always re-apply current crop, rendering this function little more than a fancy alias for
// getPlayer().
this.getPlayer();
}
forceRefreshPlayerElement() {
this.getPlayer();
}
checkPlayerSizeChange() {
// this 'if' is just here for debugging — real code starts later. It's safe to collapse and
// ignore the contents of this if (unless we need to change how logging works)
if (this.logger.canLog('debug')){
if (this.dimensions?.fullscreen){
if(! PlayerData.isFullScreen()){
this.logger.log('info', 'debug', "[PlayerDetect] player size changed. reason: exited fullscreen");
}
}
if(! this.element) {
this.logger.log('info', 'playerDetect', "[PlayerDetect] player element isn't defined");
}
if ( this.element &&
( +this.dimensions?.width != +this.element?.offsetWidth ||
+this.dimensions?.height != +this.element?.offsetHeight )
) {
this.logger.log('info', 'debug', "[PlayerDetect] player size changed. reason: dimension change. Old dimensions?", this.dimensions?.width, this.dimensions?.height, "new dimensions:", this.element?.offsetWidth, this.element?.offsetHeight);
}
}
// if size doesn't match, update & return true
if (this.dimensions?.width != this.element.offsetWidth
|| this.dimensions?.height != this.element.offsetHeight ){
const isFullScreen = PlayerData.isFullScreen();
if (isFullScreen) {
this.dimensions = {
width: window.innerWidth,
height: window.innerHeight,
fullscreen: true
}
} else {
this.dimensions = {
width: this.element.offsetWidth,
height: this.element.offsetHeight,
fullscreen: isFullScreen
};
}
return true;
}
return false;
}
checkFullscreenChange() {
const isFs = PlayerData.isFullScreen();
if (this.dimensions) {
if (this.dimensions.fullscreen != isFs) {
this.dimensions = {
fullscreen: isFs,
width: isFs ? screen.width : this.video.offsetWidth,
height: isFs ? screen.height : this.video.offsetHeight
};
return true;
}
return false;
}
this.logger.log('info', 'debug', "[PlayerData::checkFullscreenChange] this.dimensions is not defined. Assuming fs change happened and setting default values.")
this.dimensions = {
fullscreen: isFs,
width: isFs ? screen.width : this.video.offsetWidth,
height: isFs ? screen.height : this.video.offsetHeight
};
return true;
}
showNotification(notificationId) {
this.notificationService?.showNotification(notificationId);
}
/**
* NOTE: this method needs to be deleted once Edge gets its shit together.
*/
showEdgeNotification() {
// if (BrowserDetect.isEdgeUA && !this.settings.active.mutedNotifications?.browserSpecific?.edge?.brokenDrm?.[window.hostname]) {
// this.ui = new PlayerUi(this.element, this.settings);
// }
}
}
if (process.env.CHANNEL !== 'stable'){
console.info("PlayerData loaded");
}
export default PlayerData;

View File

@ -1,783 +0,0 @@
import Debug from '../../conf/Debug';
import PlayerData from './PlayerData';
import Resizer from '../video-transform/Resizer';
import ArDetector from '../ar-detect/ArDetector';
import AspectRatioType from '../../../common/enums/AspectRatioType.enum';
import * as _ from 'lodash';
import BrowserDetect from '../../conf/BrowserDetect';
import Logger from '../Logger';
import Settings from '../Settings';
import PageInfo from './PageInfo';
import { sleep } from '../../../common/js/utils';
import { hasDrm } from '../ar-detect/DrmDetecor';
class VideoData {
//#region flags
arSetupComplete: boolean = false;
destroyed: boolean = false;
invalid: boolean = false;
videoStatusOk: boolean = false;
videoLoaded: boolean = false;
videoDimensionsLoaded: boolean = false;
paused: boolean = false;
//#endregion
//#region misc stuff
vdid: string;
video: any;
observer: ResizeObserver;
mutationObserver: MutationObserver;
mutationObserverConf: MutationObserverInit = {
attributes: true,
attributeFilter: ['class', 'style'],
attributeOldValue: true,
};
extensionMode: any;
userCssClassName: string;
validationId: number;
dimensions: any;
//#endregion
//#region helper objects
logger: Logger;
settings: Settings;
pageInfo: PageInfo;
player: PlayerData;
resizer: Resizer;
arDetector: ArDetector;
//#endregion
constructor(video, settings, pageInfo){
this.vdid = (Math.random()*100).toFixed();
this.logger = pageInfo.logger;
this.arSetupComplete = false;
this.video = video;
this.destroyed = false;
this.settings = settings;
this.pageInfo = pageInfo;
this.extensionMode = pageInfo.extensionMode;
this.videoStatusOk = false;
this.userCssClassName = `uw-fuck-you-and-do-what-i-tell-you_${this.vdid}`;
this.videoLoaded = false;
this.videoDimensionsLoaded = true;
this.validationId = null;
this.dimensions = {
width: this.video.offsetWidth,
height: this.video.offsetHeight,
};
this.setupStageOne();
}
async onVideoLoaded() {
if (!this.videoLoaded) {
/**
* video.readyState 101:
* 0 no info. Can't play.
* 1 we have metadata but nothing else
* 2 we have data for current playback position, but not future <--- meaning current frame, meaning Aard can work here or higher
* 3 we have a lil bit for the future
* 4 we'll survive to the end
*/
if (!this.video?.videoWidth || !this.video?.videoHeight || this.video.readyState < 2) {
return; // onVideoLoaded is a lie in this case
}
this.logger.log('info', 'init', '%c[VideoData::onVideoLoaded] ——————————— Initiating phase two of videoData setup ———————————', 'color: #0f9');
this.videoLoaded = true;
this.videoDimensionsLoaded = true;
try {
await this.setupStageTwo();
this.logger.log('info', 'init', '%c[VideoData::onVideoLoaded] ——————————— videoData setup stage two complete ———————————', 'color: #0f9');
} catch (e) {
this.logger.log('error', 'init', '%c[VideoData::onVideoLoaded] ——————————— Setup stage two failed. ———————————\n', 'color: #f00', e);
}
} else if (!this.videoDimensionsLoaded) {
this.logger.log('info', 'debug', "%c[VideoData::restoreCrop] Recovering from illegal video dimensions. Resetting aspect ratio.", "background: #afd, color: #132");
this.restoreCrop();
this.videoDimensionsLoaded = true;
}
}
videoUnloaded() {
this.videoLoaded = false;
}
async injectBaseCss() {
try {
if (!this.mutationObserver) {
this.setupMutationObserver();
}
await this.pageInfo.injectCss(`
.uw-ultrawidify-base-wide-screen {
margin: 0px 0px 0px 0px !important;
width: initial !important;
align-self: start !important;
justify-self: start !important;
max-height: initial !important;
max-width: initial !important;
}
`);
} catch (e) {
console.error('Failed to inject base css!', e);
}
}
unsetBaseClass() {
this.mutationObserver.disconnect();
this.mutationObserver = undefined;
this.video.classList.remove('uw-ultrawidify-base-wide-screen');
}
//#region <video> event handlers
onLoadedData() {
this.logger.log('info', 'init', '[VideoData::ctor->video.onloadeddata] Video fired event "loaded data!"');
this.onVideoLoaded();
}
onLoadedMetadata() {
this.logger.log('info', 'init', '[VideoData::ctor->video.onloadedmetadata] Video fired event "loaded metadata!"');
this.onVideoLoaded();
}
onTimeUpdate() {
this.onVideoLoaded();
}
//#endregion
//#region lifecycle-ish
/**
* Injects base CSS and sets up handlers for <video> tag events
*/
async setupStageOne() {
this.logger.log('info', 'init', '%c[VideoData::setupStageOne] ——————————— Starting setup stage one! ———————————', 'color: #0f9');
// ensure base css is loaded before doing anything
this.injectBaseCss();
// this is in case extension loads before the video
this.video.addEventListener('loadeddata', this.onLoadedData.bind(this));
this.video.addEventListener('loadedmetadata', this.onLoadedMetadata.bind(this));
// this one is in case extension loads after the video is loaded
this.video.addEventListener('timeupdate', this.onTimeUpdate.bind(this));
this.logger.log('info', 'init', '%c[VideoData::setupStageOne] ——————————— Setup stage one complete! ———————————', 'color: #0f9');
}
/**
* Launches the extension for a given video (after the video element is defined well enough
* for our standards)
*/
async setupStageTwo() {
// POZOR: VRSTNI RED JE POMEMBEN (arDetect mora bit zadnji)
// NOTE: ORDERING OF OBJ INITIALIZATIONS IS IMPORTANT (arDetect needs to go last)
this.player = new PlayerData(this);
if (this.player.invalid) {
this.invalid = true;
return;
}
this.resizer = new Resizer(this);
// INIT OBSERVERS
try {
if (BrowserDetect.firefox) {
this.observer = new ResizeObserver(
_.debounce(
this.onVideoDimensionsChanged,
250,
{
leading: true,
trailing: true
}
)
);
} else {
// Chrome for some reason insists that this.onPlayerDimensionsChanged is not a function
// when it's not wrapped into an anonymous function
this.observer = new ResizeObserver(
_.debounce(
(m, o) => {
this.onVideoDimensionsChanged(m, o)
},
250,
{
leading: true,
trailing: true
}
)
);
}
} catch (e) {
console.error('[VideoData] Observer setup failed:', e);
}
this.observer.observe(this.video);
// INIT AARD
this.arDetector = new ArDetector(this); // this starts Ar detection. needs optional parameter that prevets ardetdctor from starting
// player dimensions need to be in:
// this.player.dimensions
// apply default align and stretch
this.logger.log('info', 'debug', "%c[VideoData::ctor] Initial resizer reset!", "background: #afd, color: #132");
this.resizer.reset();
this.logger.log('info', ['debug', 'init'], '[VideoData::ctor] Created videoData with vdid', this.vdid, '\nextension mode:', this.extensionMode)
this.pageInfo.initMouseActionHandler(this);
// NOTE — since base class for our <video> element depends on player aspect ratio,
// we handle it in PlayerData class.
this.video.classList.add('uw-ultrawidify-base-wide-screen');
this.video.classList.add(this.userCssClassName); // this also needs to be applied BEFORE we initialize resizer!
// start fallback video/player size detection
this.fallbackChangeDetection();
// force reload last aspect ratio (if default crop ratio exists), but only after the video is
if (this.pageInfo.defaultCrop) {
this.resizer.setAr(this.pageInfo.defaultCrop);
}
try {
if (!this.pageInfo.defaultCrop) {
if (!this.invalid) {
this.initArDetection();
} else {
this.logger.log('error', 'debug', '[VideoData::secondStageSetup] Video is invalid. Aard not started.', this.video);
}
} else {
this.logger.log('info', 'debug', '[VideoData::secondStageSetup] Default crop is specified for this site. Not starting aard.');
}
} catch (e) {
this.logger.log('error', 'init', `[VideoData::secondStageSetup] Error with aard initialization (or error with default aspect ratio application)`, e)
}
}
setupMutationObserver() {
try {
if (BrowserDetect.firefox) {
this.mutationObserver = new MutationObserver(
_.debounce(
this.onVideoMutation,
250,
{
leading: true,
trailing: true
}
)
)
} else {
// Chrome for some reason insists that this.onPlayerDimensionsChanged is not a function
// when it's not wrapped into an anonymous function
this.mutationObserver = new MutationObserver(
_.debounce(
(m, o) => {
this.onVideoMutation(m, o)
},
250,
{
leading: true,
trailing: true
}
)
)
}
} catch (e) {
console.error('[VideoData] Observer setup failed:', e);
}
this.mutationObserver.observe(this.video, this.mutationObserverConf);
}
/**
* cleans up handlers and stuff when the show is over
*/
destroy() {
this.logger.log('info', ['debug', 'init'], `[VideoData::destroy] <vdid:${this.vdid}> received destroy command`);
if (this.video) {
this.video.classList.remove(this.userCssClassName);
this.video.classList.remove('uw-ultrawidify-base-wide-screen');
this.video.removeEventListener('onloadeddata', this.onLoadedData);
this.video.removeEventListener('onloadedmetadata', this.onLoadedMetadata);
this.video.removeEventListener('ontimeupdate', this.onTimeUpdate);
}
this.pause();
this.destroyed = true;
try {
this.arDetector.stop();
this.arDetector.destroy();
} catch (e) {}
this.arDetector = undefined;
try {
this.resizer.destroy();
} catch (e) {}
this.resizer = undefined;
try {
this.player.destroy();
} catch (e) {}
try {
this.observer.disconnect();
} catch (e) {}
this.player = undefined;
this.video = undefined;
}
//#endregion
//#region video status
isVideoPlaying() {
return this.video && !!(this.video.currentTime > 0 && !this.video.paused && !this.video.ended && this.video.readyState > 2);
}
hasVideoStartedPlaying() {
return this.video && this.video.currentTime > 0;
}
//#endregion
restoreCrop() {
this.logger.log('info', 'debug', '[VideoData::restoreCrop] Attempting to reset aspect ratio.')
// if we have default crop set for this page, apply this.
// otherwise, reset crop
if (this.pageInfo.defaultCrop) {
this.resizer.setAr(this.pageInfo.defaultCrop);
} else {
this.resizer.reset();
try {
this.stopArDetection();
this.startArDetection();
} catch (e) {
this.logger.log('warn', 'debug', '[VideoData::restoreCrop] Autodetection not resumed. Reason:', e);
}
}
}
/**
* Starts fallback change detection (validates whether currently applied settings are correct)
*/
async fallbackChangeDetection() {
const validationId = Date.now();
this.validationId = validationId;
while (!this.destroyed && !this.invalid && this.validationId === validationId) {
await sleep(500);
this.doPeriodicFallbackChangeDetectionCheck();
}
}
doPeriodicFallbackChangeDetectionCheck() {
this.validateVideoOffsets();
}
onVideoMutation(mutationList?: MutationRecord[], observer?) {
// verify that mutation didn't remove our class. Some pages like to do that.
let confirmAspectRatioRestore = false;
for(const mutation of mutationList) {
if (mutation.type === 'attributes') {
if( mutation.attributeName === 'class'
&& mutation.oldValue.indexOf('uw-ultrawidify-base-wide-screen') !== -1
&& !this.video.classList.contains('uw-ultrawidify-base-wide-screen')
) {
// force the page to include our class in classlist, if the classlist has been removed
// while classList.add() doesn't duplicate classes (does nothing if class is already added),
// we still only need to make sure we're only adding our class to classlist if it has been
// removed. classList.add() will _still_ trigger mutation (even if classlist wouldn't change).
// This is a problem because INFINITE RECURSION TIME, and we _really_ don't want that.
confirmAspectRatioRestore = true;
this.video.classList.add(this.userCssClassName);
this.video.classList.add('uw-ultrawidify-base-wide-screen');
} else if (mutation.attributeName === 'style') {
confirmAspectRatioRestore = true;
}
}
}
this.processDimensionsChanged();
}
onVideoDimensionsChanged(mutationList, observer) {
if (!mutationList || this.video === undefined) { // something's wrong
if (observer && this.video) {
observer.disconnect();
}
return;
}
this.processDimensionsChanged();
}
/**
* Forces Ultrawidify to resotre aspect ratio. You should never call this method directly,
* instead you should be calling processDimensionChanged() wrapper function.
*/
private _processDimensionsChanged() {
// adding player observer taught us that if element size gets triggered by a class, then
// the 'style' attributes don't necessarily trigger. This means we also need to trigger
// restoreAr here, in case video size was changed this way
this.player.forceRefreshPlayerElement();
this.restoreAr();
// sometimes something fucky wucky happens and mutations aren't detected correctly, so we
// try to get around that
setTimeout( () => {
this.validateVideoOffsets();
}, 100);
}
/**
* Restores aspect ratio and validates video offsets after the restore. Execution uses
* debounce to limit how often the function executes.
*/
private processDimensionsChanged() {
_.debounce(
this._processDimensionsChanged,
250,
{
leading: true,
trailing: true
}
);
}
validateVideoOffsets() {
// validate if current video still exists. If not, we destroy current object
try {
if (! document.body.contains(this.video)) {
this.destroy();
return;
}
} catch (e) {
}
// THIS BREAKS PANNING
const cs = window.getComputedStyle(this.video);
const pcs = window.getComputedStyle(this.player.element);
try {
const transformMatrix = cs.transform.split(')')[0].split(',');
const translateX = +transformMatrix[4];
const translateY = +transformMatrix[5];
const vh = +(cs.height.split('px')[0]);
const vw = +(cs.width.split('px')[0]);
const ph = +(pcs.height.split('px')[0]);
const pw = +(pcs.width.split('px')[0]);
// TODO: check & account for panning and alignment
if (transformMatrix[0] !== 'none'
&& this.isWithin(vh, (ph - (translateY * 2)), 2)
&& this.isWithin(vw, (pw - (translateX * 2)), 2)) {
} else {
this.player.forceDetectPlayerElementChange();
}
} catch(e) {
console.error('Validating video offsets failed:', e)
}
}
isWithin(a, b, diff) {
return a < b + diff && a > b - diff
}
/**
* Gets the contents of the style attribute of the video element
* in a form of an object.
*/
getVideoStyle(): any {
// This will _always_ give us an array. Empty string gives an array
// that contains one element. That element is an empty string.
const styleArray = (this.video.getAttribute('style') || '').split(';');
const styleObject = {};
for (const style of styleArray) {
// not a valid CSS, so we skip those
if (style.indexOf(':') === -1) {
continue;
}
// let's play _very_ safe
let [property, value] = style.split('!important')[0].split(':');
value = value.trim();
styleObject[property] = value;
}
return styleObject;
}
/**
* Some sites try to accomodate ultrawide users by "cropping" videos
* by setting 'style' attribute of the video element to 'height: X%',
* where 'X' is something greater than 100.
*
* This function gets that percentage and converts it into a factor.
*/
getHeightCompensationFactor() {
const heightStyle = this.getVideoStyle()?.height;
if (!heightStyle || !heightStyle.endsWith('%')) {
return 1;
}
const heightCompensationFactor = heightStyle.split('%')[0] / 100;
if (isNaN(heightCompensationFactor)) {
return 1;
}
return heightCompensationFactor;
}
firstTimeArdInit(){
if(this.destroyed || this.invalid) {
// throw {error: 'VIDEO_DATA_DESTROYED', data: {videoData: this}};
return;
}
if(! this.arSetupComplete){
this.arDetector = new ArDetector(this);
}
}
initArDetection() {
if(this.destroyed || this.invalid) {
// throw {error: 'VIDEO_DATA_DESTROYED', data: {videoData: this}};
return;
}
if (this.arDetector){
this.arDetector.init();
}
else{
this.arDetector = new ArDetector(this);
this.arDetector.init();
}
}
startArDetection() {
this.logger.log('info', 'debug', "[VideoData::startArDetection] starting AR detection")
if(this.destroyed || this.invalid) {
// throw {error: 'VIDEO_DATA_DESTROYED', data: {videoData: this}};
return;
}
if (hasDrm(this.video)) {
this.player.showNotification('AARD_DRM');
}
if (!this.arDetector) {
this.initArDetection();
}
this.arDetector.start();
}
rebootArDetection() {
if(this.destroyed || this.invalid) {
// throw {error: 'VIDEO_DATA_DESTROYED', data: {videoData: this}};
return;
}
this.arDetector.init();
}
stopArDetection() {
if (this.arDetector) {
this.arDetector.stop();
}
}
pause(){
this.paused = true;
if(this.arDetector){
this.arDetector.stop();
}
if(this.player){
this.player.stop();
}
}
resume(){
if(this.destroyed || this.invalid) {
// throw {error: 'VIDEO_DATA_DESTROYED', data: {videoData: this}};
return;
}
this.paused = false;
try {
// this.resizer.start();
if (this.player) {
this.player.start();
}
} catch (e) {
this.logger.log('error', 'debug', "[VideoData.js::resume] cannot resume for reasons. Will destroy videoData. Error here:", e);
this.destroy();
}
}
resumeAutoAr(){
if(this.arDetector){
this.startArDetection();
}
}
setManualTick(manualTick) {
if(this.arDetector){
this.arDetector.setManualTick(manualTick);
}
}
tick() {
if(this.arDetector){
this.arDetector.tick();
}
}
setLastAr(lastAr){
if (this.invalid) {
return;
}
this.resizer.setLastAr(lastAr);
}
setAr(ar, lastAr?){
if (this.invalid) {
return;
}
if (ar.type === AspectRatioType.Fixed || ar.type === AspectRatioType.FitHeight || ar.type === AspectRatioType.FitHeight) {
this.player.forceRefreshPlayerElement();
}
this.resizer.setAr(ar, lastAr);
}
resetAr() {
if (this.invalid) {
return;
}
this.resizer.reset();
}
resetLastAr() {
if (this.invalid) {
return;
}
this.resizer.setLastAr('original');
}
panHandler(event, forcePan?: boolean) {
if (this.invalid) {
return;
}
if(this.destroyed) {
// throw {error: 'VIDEO_DATA_DESTROYED', data: {videoData: this}};
return;
}
if(!this.resizer) {
this.destroy();
return;
}
this.resizer.panHandler(event, forcePan);
}
setPanMode(mode) {
if (this.invalid) {
return;
}
this.resizer.setPanMode(mode);
}
setVideoAlignment(videoAlignment) {
if (this.invalid) {
return;
}
this.resizer.setVideoAlignment(videoAlignment);
}
restoreAr(){
if (this.invalid) {
return;
}
this.resizer.restore();
}
setStretchMode(stretchMode, fixedStretchRatio){
if (this.invalid) {
return;
}
this.resizer.setStretchMode(stretchMode, fixedStretchRatio);
}
setZoom(zoomLevel, no_announce){
if (this.invalid) {
return;
}
this.resizer.setZoom(zoomLevel, no_announce);
}
zoomStep(step){
if (this.invalid) {
return;
}
this.resizer.zoomStep(step);
}
announceZoom(scale){
if (this.invalid) {
return;
}
this.pageInfo.announceZoom(scale);
}
markPlayer(name, color) {
if (this.invalid) {
return;
}
if (this.player) {
this.player.markPlayer(name, color)
}
}
unmarkPlayer() {
this.player.unmarkPlayer();
}
isPlaying() {
return this.video && this.video.currentTime > 0 && !this.video.paused && !this.video.ended;
}
checkVideoSizeChange(){
const videoWidth = this.video.offsetWidth;
const videoHeight = this.video.offsetHeight;
// this 'if' is just here for debugging — real code starts later. It's safe to collapse and
// ignore the contents of this if (unless we need to change how logging works)
if (this.logger.canLog('debug')){
if(! this.video) {
this.logger.log('info', 'videoDetect', "[VideoDetect] player element isn't defined");
}
if ( this.video &&
( this.dimensions?.width != videoWidth ||
this.dimensions?.height != videoHeight )
) {
this.logger.log('info', 'debug', "[VideoDetect] player size changed. reason: dimension change. Old dimensions?", this.dimensions.width, this.dimensions.height, "new dimensions:", this.video.offsetWidth, this.video.offsetHeight);
}
}
// if size doesn't match, update & return true
if (this.dimensions?.width != videoWidth
|| this.dimensions?.height != videoHeight ){
this.dimensions = {
width: videoWidth,
height: videoHeight,
};
return true;
}
return false;
}
}
export default VideoData;

View File

@ -1,756 +0,0 @@
import Debug from '../../conf/Debug';
import Scaler, { CropStrategy, VideoDimensions } from './Scaler';
import Stretcher from './Stretcher';
import Zoom from './Zoom';
import PlayerData from '../video-data/PlayerData';
import ExtensionMode from '../../../common/enums/ExtensionMode.enum';
import StretchType from '../../../common/enums/StretchType.enum';
import VideoAlignmentType from '../../../common/enums/VideoAlignmentType.enum';
import AspectRatioType from '../../../common/enums/AspectRatioType.enum';
import CropModePersistance from '../../../common/enums/CropModePersistence.enum';
import { sleep } from '../Util';
import Logger from '../Logger';
import Settings from '../Settings';
import VideoData from '../video-data/VideoData';
if(Debug.debug) {
console.log("Loading: Resizer.js");
}
class Resizer {
//#region flags
canPan: boolean = false;
destroyed: boolean = false;
//#endregion
//#region helper objects
logger: Logger;
settings: Settings;
scaler: Scaler;
stretcher: Stretcher;
zoom: Zoom;
conf: VideoData;
//#endregion
//#region HTML elements
video: any;
//#endregion
//#region data
correctedVideoDimensions: any;
currentCss: any;
currentStyleString: string;
currentPlayerStyleString: any;
currentCssValidFor: any;
currentVideoSettings: any;
lastAr: {type: any, ratio?: number} = {type: AspectRatioType.Initial};
resizerId: any;
videoAlignment: any;
userCss: string;
userCssClassName: any;
pan: any = null;
//#endregion
constructor(videoData) {
this.resizerId = (Math.random()*100).toFixed(0);
this.conf = videoData;
this.logger = videoData.logger;
this.video = videoData.video;
this.settings = videoData.settings;
this.scaler = new Scaler(this.conf);
this.stretcher = new Stretcher(this.conf);
this.zoom = new Zoom(this.conf);
this.videoAlignment = this.settings.getDefaultVideoAlignment(window.location.hostname); // this is initial video alignment
this.destroyed = false;
if (this.settings.active.pan) {
this.canPan = this.settings.active.miscSettings.mousePan.enabled;
} else {
this.canPan = false;
}
this.userCssClassName = videoData.userCssClassName;
}
injectCss(css) {
this.conf.pageInfo.injectCss(css);
}
ejectCss(css) {
this.conf.pageInfo.ejectCss(css);
}
replaceCss(oldCss, newCss) {
this.conf.pageInfo.replaceCss(oldCss, newCss);
}
prepareCss(css) {
return `.${this.userCssClassName} {${css}}`;
}
destroy(){
this.logger.log('info', ['debug', 'init'], `[Resizer::destroy] <rid:${this.resizerId}> received destroy command.`);
this.destroyed = true;
}
calculateRatioForLegacyOptions(ar){
// also present as modeToAr in Scaler.js
if (ar.type !== AspectRatioType.FitWidth && ar.type !== AspectRatioType.FitHeight && ar.ratio) {
return ar;
}
// Skrbi za "stare" možnosti, kot na primer "na širino zaslona", "na višino zaslona" in "ponastavi".
// Približevanje opuščeno.
// handles "legacy" options, such as 'fit to widht', 'fit to height' and AspectRatioType.Reset. No zoom tho
let ratioOut;
if (!this.conf.video) {
this.logger.log('info', 'debug', "[Scaler.js::modeToAr] No video??",this.conf.video, "killing videoData");
this.conf.destroy();
return null;
}
if (! this.conf.player.dimensions) {
ratioOut = screen.width / screen.height;
} else {
this.logger.log('info', 'debug', `[Resizer::calculateRatioForLegacyOptions] <rid:${this.resizerId}> Player dimensions:`, this.conf.player.dimensions.width ,'x', this.conf.player.dimensions.height,'aspect ratio:', this.conf.player.dimensions.width / this.conf.player.dimensions.height)
ratioOut = this.conf.player.dimensions.width / this.conf.player.dimensions.height;
}
// POMEMBNO: lastAr je potrebno nastaviti šele po tem, ko kličemo _res_setAr(). _res_setAr() predvideva,
// da želimo nastaviti statično (type: 'static') razmerje stranic — tudi, če funkcijo kličemo tu oz. v ArDetect.
//
// IMPORTANT NOTE: lastAr needs to be set after _res_setAr() is called, as _res_setAr() assumes we're
// setting a static aspect ratio (even if the function is called from here or ArDetect).
let fileAr = this.conf.video.videoWidth / this.conf.video.videoHeight;
if (ar.type === AspectRatioType.FitWidth){
ar.ratio = ratioOut > fileAr ? ratioOut : fileAr;
}
else if(ar.type === AspectRatioType.FitHeight){
ar.ratio = ratioOut < fileAr ? ratioOut : fileAr;
}
else if(ar.type === AspectRatioType.Reset){
this.logger.log('info', 'debug', "[Scaler.js::modeToAr] Using original aspect ratio -", fileAr);
ar.ratio = fileAr;
} else {
return null;
}
return ar;
}
updateAr(ar) {
if (!ar) {
return;
}
// Some options require a bit more testing re: whether they make sense
// if they don't, we refuse to update aspect ratio until they do
if (ar.type === AspectRatioType.Automatic || ar.type === AspectRatioType.Fixed) {
if (!ar.ratio || isNaN(ar.ratio)) {
return;
}
}
// Only update aspect ratio if there's a difference between the old and the new state
if (!this.lastAr || ar.type !== this.lastAr.type || ar.ratio !== this.lastAr.ratio) {
this.setAr(ar);
}
}
async setAr(ar: {type: any, ratio?: number}, lastAr?: {type: any, ratio?: number}) {
if (this.destroyed) {
return;
}
if (!this.video.videoWidth || !this.video.videoHeight) {
this.logger.log('warning', 'debug', '[Resizer::setAr] <rid:'+this.resizerId+'> Video has no width or no height. This is not allowed. Aspect ratio will not be set, and videoData will be uninitialized.');
this.conf.videoUnloaded();
}
this.logger.log('info', 'debug', '[Resizer::setAr] <rid:'+this.resizerId+'> trying to set ar. New ar:', ar)
if (ar == null) {
return;
}
const siteSettings = this.settings.active.sites[window.location.hostname];
let stretchFactors: {xFactor: number, yFactor: number, arCorrectionFactor?: number, ratio?: number} | any;
// reset zoom, but only on aspect ratio switch. We also know that aspect ratio gets converted to
// AspectRatioType.Fixed when zooming, so let's keep that in mind
if (
(ar.type !== AspectRatioType.Fixed && ar.type !== AspectRatioType.Manual) // anything not these two _always_ changes AR
|| ar.type !== this.lastAr.type // this also means aspect ratio has changed
|| ar.ratio !== this.lastAr.ratio // this also means aspect ratio has changed
) {
this.zoom.reset();
this.resetPan();
}
// most everything that could go wrong went wrong by this stage, and returns can happen afterwards
// this means here's the optimal place to set or forget aspect ratio. Saving of current crop ratio
// is handled in pageInfo.updateCurrentCrop(), which also makes sure to persist aspect ratio if ar
// is set to persist between videos / through current session / until manual reset.
if (ar.type === AspectRatioType.Automatic ||
ar.type === AspectRatioType.Reset ||
ar.type === AspectRatioType.Initial ) {
// reset/undo default
this.conf.pageInfo.updateCurrentCrop(undefined);
} else {
this.conf.pageInfo.updateCurrentCrop(ar);
}
if (lastAr) {
this.lastAr = this.calculateRatioForLegacyOptions(lastAr);
ar = this.calculateRatioForLegacyOptions(ar);
} else {
// NOTE: "fitw" "fith" and "reset" should ignore ar.ratio bit, but
// I'm not sure whether they do. Check that.
ar = this.calculateRatioForLegacyOptions(ar);
if (! ar) {
this.logger.log('info', 'resizer', `[Resizer::setAr] <${this.resizerId}> Something wrong with ar or the player. Doing nothing.`);
return;
}
this.lastAr = {type: ar.type, ratio: ar.ratio}
}
// if (this.extensionMode === ExtensionMode.Basic && !PlayerData.isFullScreen() && ar.type !== AspectRatioType.Reset) {
// // don't actually apply or calculate css when using basic mode if not in fullscreen
// // ... unless we're resetting the aspect ratio to original
// return;
// }
if (! this.video) {
this.conf.destroy();
}
// pause AR on:
// * ar.type NOT automatic
// * ar.type is auto, but stretch is set to basic basic stretch
//
// unpause when using other modes
if (ar.type !== AspectRatioType.Automatic || this.stretcher.mode === StretchType.Basic) {
this.conf?.arDetector?.pause();
} else {
if (this.lastAr.type === AspectRatioType.Automatic) {
this.conf?.arDetector?.unpause();
}
}
// do stretch thingy
if (this.stretcher.mode === StretchType.NoStretch
|| this.stretcher.mode === StretchType.Conditional
|| this.stretcher.mode === StretchType.FixedSource){
stretchFactors = this.scaler.calculateCrop(ar);
if(! stretchFactors || stretchFactors.error){
this.logger.log('error', 'debug', `[Resizer::setAr] <rid:${this.resizerId}> failed to set AR due to problem with calculating crop. Error:`, stretchFactors?.error);
if (stretchFactors?.error === 'no_video'){
this.conf.destroy();
return;
}
// we could have issued calculate crop too early. Let's tell VideoData that there's something wrong
// and exit this function. When <video> will receive onloadeddata or ontimeupdate (receiving either
// of the two means video is loaded or playing, and that means video has proper dimensions), it will
// try to reset or re-apply aspect ratio when the video is finally ready.
if (stretchFactors?.error === 'illegal_video_dimensions') {
this.conf.videoDimensionsLoaded = false;
return;
}
}
if (this.stretcher.mode === StretchType.Conditional){
this.stretcher.applyConditionalStretch(stretchFactors, ar.ratio);
} else if (this.stretcher.mode === StretchType.FixedSource) {
this.stretcher.applyStretchFixedSource(stretchFactors);
}
this.logger.log('info', 'debug', "[Resizer::setAr] Processed stretch factors for ",
this.stretcher.mode === StretchType.NoStretch ? 'stretch-free crop.' :
this.stretcher.mode === StretchType.Conditional ? 'crop with conditional StretchType.' : 'crop with fixed stretch',
'Stretch factors are:', stretchFactors
);
} else if (this.stretcher.mode === StretchType.Hybrid) {
stretchFactors = this.stretcher.calculateStretch(ar.ratio);
this.logger.log('info', 'debug', '[Resizer::setAr] Processed stretch factors for hybrid stretch/crop. Stretch factors are:', stretchFactors);
} else if (this.stretcher.mode === StretchType.Fixed) {
stretchFactors = this.stretcher.calculateStretchFixed(ar.ratio)
} else if (this.stretcher.mode === StretchType.Basic) {
stretchFactors = this.stretcher.calculateBasicStretch();
this.logger.log('info', 'debug', '[Resizer::setAr] Processed stretch factors for basic StretchType. Stretch factors are:', stretchFactors);
} else {
stretchFactors = {xFactor: 1, yFactor: 1};
this.logger.log('error', 'debug', '[Resizer::setAr] Okay wtf happened? If you see this, something has gone wrong', stretchFactors,"\n------[ i n f o d u m p ]------\nstretcher:", this.stretcher);
}
this.zoom.applyZoom(stretchFactors);
this.stretcher.chromeBugMitigation(stretchFactors);
let translate = this.computeOffsets(stretchFactors);
this.applyCss(stretchFactors, translate);
}
toFixedAr() {
// converting to fixed AR means we also turn off autoAR
this.setAr({
ratio: this.lastAr.ratio,
type: AspectRatioType.Fixed
});
}
resetLastAr() {
this.lastAr = {type: AspectRatioType.Initial};
}
setLastAr(override){
this.lastAr = override;
}
getLastAr(){
return this.lastAr;
}
setStretchMode(stretchMode, fixedStretchRatio?){
this.stretcher.setStretchMode(stretchMode, fixedStretchRatio);
this.restore();
}
panHandler(event, forcePan) {
if (this.canPan || forcePan) {
if(!this.conf.player || !this.conf.player.element) {
return;
}
// dont allow weird floats
this.videoAlignment = VideoAlignmentType.Center;
// because non-fixed aspect ratios reset panning:
if (this.lastAr.type !== AspectRatioType.Fixed) {
this.toFixedAr();
}
const player = this.conf.player.element;
const relativeX = (event.pageX - player.offsetLeft) / player.offsetWidth;
const relativeY = (event.pageY - player.offsetTop) / player.offsetHeight;
this.logger.log('info', 'mousemove', "[Resizer::panHandler] mousemove.pageX, pageY:", event.pageX, event.pageY, "\nrelativeX/Y:", relativeX, relativeY)
this.setPan(relativeX, relativeY);
}
}
resetPan() {
this.pan = {x: 0, y: 0};
this.videoAlignment = this.settings.getDefaultVideoAlignment(window.location.hostname);
}
setPan(relativeMousePosX, relativeMousePosY){
// relativeMousePos[X|Y] - on scale from 0 to 1, how close is the mouse to player edges.
// use these values: top, left: 0, bottom, right: 1
if(! this.pan){
this.pan = {x: 0, y: 0};
}
if (this.settings.active.miscSettings.mousePanReverseMouse) {
this.pan.relativeOffsetX = (relativeMousePosX * 1.1) - 0.55;
this.pan.relativeOffsetY = (relativeMousePosY * 1.1) - 0.55;
} else {
this.pan.relativeOffsetX = -(relativeMousePosX * 1.1) + 0.55;
this.pan.relativeOffsetY = -(relativeMousePosY * 1.1) + 0.55;
}
this.restore();
}
setVideoAlignment(videoAlignment) {
this.videoAlignment = videoAlignment;
this.restore();
}
restore() {
this.logger.log('info', 'debug', "[Resizer::restore] <rid:"+this.resizerId+"> attempting to restore aspect ratio", {'lastAr': this.lastAr} );
// this is true until we verify that css has actually been applied
if(this.lastAr.type === AspectRatioType.Initial){
this.setAr({type: AspectRatioType.Reset});
}
else {
if (this.lastAr?.ratio === null) {
// if this is the case, we do nothing as we have the correct aspect ratio
// throw "Last ar is null!"
return;
}
this.setAr(this.lastAr, this.lastAr)
}
}
reset(){
this.setStretchMode(this.settings.active.sites[window.location.hostname]?.stretch ?? this.settings.active.sites['@global'].stretch);
this.zoom.setZoom(1);
this.resetPan();
this.setAr({type: AspectRatioType.Reset});
}
setPanMode(mode) {
if (mode === 'enable') {
this.canPan = true;
} else if (mode === 'disable') {
this.canPan = false;
} else if (mode === 'toggle') {
this.canPan = !this.canPan;
}
}
setZoom(zoomLevel, no_announce) {
this.zoom.setZoom(zoomLevel, no_announce);
}
zoomStep(step){
this.zoom.zoomStep(step);
}
resetZoom(){
this.zoom.setZoom(1);
this.restore();
}
resetCrop(){
this.setAr({type: AspectRatioType.Reset});
}
resetStretch(){
this.stretcher.setStretchMode(StretchType.NoStretch);
this.restore();
}
// mostly internal stuff
/**
* Returns the size of the video file _as displayed_ on the screen.
* Consider the following example:
*
* * player dimensions are 2560x1080
* * <video> is child of player
* * <video> has the following css: {width: 100%, height: 100%}
* * video file dimensions are 1280x720
*
* CSS will ensure that the dimensions of <video> tag are equal to the dimension of the
* player element that is, 2560x1080px. This is no bueno, because the browser will upscale
* the video file to take up as much space as it can (without stretching it). This means
* we'll get a 1920x1080 video (as displayed) and a letterbox.
*
* We can't get that number out of anywhere: video.videoWidth will return 1280 (video file
* dimensions) and .offsetWidth (and the likes) will return the <video> tag dimension. Neither
* will return the actual size of video as displayed, which we need in order to calculate the
* extra space to the left and right of the video.
*
* We make the assumption of the
*/
computeVideoDisplayedDimensions() {
const offsetWidth = this.conf.video.offsetWidth;
const offsetHeight = this.conf.video.offsetHeight;
const scaleX = offsetWidth / this.conf.video.videoWidth;
const scaleY = offsetHeight / this.conf.video.videoHeight;
// if differences between the scale factors are minimal, we presume offsetWidth and
// offsetHeight are the accurate enough for our needs
if (Math.abs(scaleX - scaleY) < 0.02) {
return {
realVideoWidth: offsetWidth,
realVideoHeight: offsetHeight,
marginX: 0,
marginY: 0,
}
}
// if we're still here, we need to calculate real video dimensions
const diffX = Math.abs(scaleY * this.conf.video.videoWidth - offsetWidth);
const diffY = Math.abs(scaleX * this.conf.video.videoHeight - offsetHeight);
// in this case, we want to base our real dimensions off scaleX
// otherwise, we want to base it off scaleY
if (diffX < diffY) {
const realHeight = this.conf.video.videoHeight * scaleX;
return {
realVideoWidth: offsetWidth,
realVideoHeight: realHeight,
marginX: 0,
marginY: (offsetHeight - realHeight) * 0.5
}
} else {
const realWidth = this.conf.video.videoWidth * scaleY;
return {
realVideoWidth: realWidth,
realVideoHeight: offsetHeight,
marginX: (offsetWidth - realWidth) * 0.5,
marginY: 0
}
}
}
/**
* Sometimes, sites (e.g. new reddit) will guarantee that video fits width of its container
* and let the browser figure out the height through the magic of height:auto. This is bad,
* because our addon generally relies of videos always being 100% of the height of the
* container.
*
* This sometimes leads to a situation where realVideoHeight and realVideoWidth at least
* one of which should be roughly equal to the player width or hight with the other one being
* either smaller or equal are both smaller than player width or height; and sometimes
* rather substantially. Fortunately for us, realVideo[Width|Height] and player dimensions
* never lie, which allows us to calculate the extra scale factor we need.
*
* Returned factor for this function should do fit:contain, not fit:cover.
* @param realVideoWidth real video width
* @param realVideoHeight real video height
* @param playerWidth player width
* @param playerHeight player height
* @param mode whether to
*/
computeAutoHeightCompensationFactor(realVideoWidth: number, realVideoHeight: number, playerWidth: number, playerHeight: number, mode: 'height' | 'width'): number {
const widthFactor = playerWidth / realVideoWidth;
const heightFactor = playerHeight / realVideoHeight;
return mode === 'height' ? heightFactor : widthFactor;
}
private _computeOffsetsRecursionGuard: boolean = false;
computeOffsets(stretchFactors: VideoDimensions){
this.logger.log('info', 'debug', "[Resizer::computeOffsets] <rid:"+this.resizerId+"> video will be aligned to ", this.settings.active.sites['@global'].videoAlignment);
const {realVideoWidth, realVideoHeight, marginX, marginY} = this.computeVideoDisplayedDimensions();
// correct any remaining element size discrepencies (applicable only to certain crop strategies!)
// NOTE: it's possible that we might also need to apply a similar measure for CropPillarbox strategy
// (but we'll wait for bug reports before doing so).
// We also don't compensate for height:auto if height is provided via element style
let autoHeightCompensationFactor;
if (
stretchFactors.cropStrategy === CropStrategy.CropLetterbox
&& (!stretchFactors.styleHeightCompensationFactor || stretchFactors.styleHeightCompensationFactor === 1)
) {
autoHeightCompensationFactor = this.computeAutoHeightCompensationFactor(realVideoWidth, realVideoHeight, this.conf.player.dimensions.width, this.conf.player.dimensions.height, 'height');
stretchFactors.xFactor *= autoHeightCompensationFactor;
stretchFactors.yFactor *= autoHeightCompensationFactor;
}
const wdiff = this.conf.player.dimensions.width - realVideoWidth;
const hdiff = this.conf.player.dimensions.height - realVideoHeight;
const wdiffAfterZoom = realVideoWidth * stretchFactors.xFactor - this.conf.player.dimensions.width;
const hdiffAfterZoom = realVideoHeight * stretchFactors.yFactor - this.conf.player.dimensions.height;
const translate = {
x: wdiff * 0.5,
y: hdiff * 0.5,
};
if (this.pan.relativeOffsetX || this.pan.relativeOffsetY) {
// don't offset when video is smaller than player
if(wdiffAfterZoom >= 0 || hdiffAfterZoom >= 0) {
translate.x += wdiffAfterZoom * this.pan.relativeOffsetX * this.zoom.scale;
translate.y += hdiffAfterZoom * this.pan.relativeOffsetY * this.zoom.scale;
}
} else {
if (this.videoAlignment == VideoAlignmentType.Left) {
translate.x += wdiffAfterZoom * 0.5;
}
else if (this.videoAlignment == VideoAlignmentType.Right) {
translate.x -= wdiffAfterZoom * 0.5;
}
}
this.logger.log(
'info', ['debug', 'resizer'], "[Resizer::_res_computeOffsets] <rid:"+this.resizerId+"> calculated offsets:",
'\n\n---- elements ----',
'\nplayer element: ', this.conf.player.element,
'\nvideo element: ', this.conf.video,
'\n\n---- data in ----',
'\nplayer dimensions: ', {w: this.conf.player.dimensions.width, h: this.conf.player.dimensions.height},
'\nvideo dimensions: ', {w: this.conf.video.offsetWidth, h: this.conf.video.offsetHeight},
'\nreal video dimensions:', {w: realVideoWidth, h: realVideoHeight},
'\nauto compensation: ', 'x', autoHeightCompensationFactor,
'\nstretch factors: ', stretchFactors,
'\npan & zoom: ', this.pan, this.zoom.scale,
'\nwdiff, hdiff: ', wdiff, 'x', hdiff,
'\nwdiff, hdiffAfterZoom:', wdiffAfterZoom, 'x', hdiffAfterZoom,
'\n\n---- data out ----\n',
'translate:', translate
);
// by the way, let's do a quick sanity check whether video player is doing any fuckies wuckies
// fucky wucky examples:
//
// * video width is bigger than player width AND video height is bigger than player height
// * video width is smaller than player width AND video height is smaller than player height
//
// In both examples, at most one of the two conditions can be true at the same time. If both
// conditions are true at the same time, we need to go 'chiny reckon' and recheck our player
// element. Chances are our video is not getting aligned correctly
if (
(this.conf.video.offsetWidth > this.conf.player.dimensions.width && this.conf.video.offsetHeight > this.conf.player.dimensions.height) ||
(this.conf.video.offsetWidth < this.conf.player.dimensions.width && this.conf.video.offsetHeight < this.conf.player.dimensions.height)
) {
this.logger.log('warn', ['debugger', 'resizer'], `[Resizer::_res_computeOffsets] <rid:${this.resizerId}> We are getting some incredibly funny results here.\n\n`,
`Video seems to be both wider and taller (or shorter and narrower) than player element at the same time. This is super duper not supposed to happen.\n\n`,
`Player element needs to be checked.`
)
// sometimes this appears to randomly recurse.
// There seems to be no way to reproduce it.
if (! this._computeOffsetsRecursionGuard) {
this._computeOffsetsRecursionGuard = true;
if (this.conf.player.checkPlayerSizeChange()) {
this.conf.player.onPlayerDimensionsChanged();
}
this._computeOffsetsRecursionGuard = false;
}
}
return translate;
}
//#region css handling
buildStyleArray(existingStyleString, extraStyleString) {
if (existingStyleString) {
const styleArray = existingStyleString.split(";");
if (extraStyleString) {
const extraCss = extraStyleString.split(';');
let dup = false;
for (const ecss of extraCss) {
for (let i in styleArray) {
if (ecss.split(':')[0].trim() === styleArray[i].split(':')[0].trim()) {
dup = true;
styleArray[i] = ecss;
}
if (dup) {
dup = false;
continue;
}
styleArray.push(ecss);
}
}
}
for (let i in styleArray) {
styleArray[i] = styleArray[i].trim();
// some sites do 'top: 50%; left: 50%; transform: <transform>' to center videos.
// we dont wanna, because we already center videos on our own
if (styleArray[i].startsWith("transform:")
|| styleArray[i].startsWith("top:")
|| styleArray[i].startsWith("left:")
|| styleArray[i].startsWith("right:")
|| styleArray[i].startsWith("bottom:")
|| styleArray[i].startsWith("margin")
){
delete styleArray[i];
}
}
return styleArray;
}
return [];
}
buildStyleString(styleArray) {
let styleString = '';
for(let i in styleArray) {
if(styleArray[i]) {
styleString += styleArray[i] + " !important; ";
}
}
return styleString;
}
applyCss(stretchFactors, translate){
// apply extra CSS here. In case of duplicated properties, extraCss overrides
// default styleString
if (! this.video) {
this.logger.log('warn', 'debug', "[Resizer::applyCss] <rid:"+this.resizerId+"> Video went missing, doing nothing.");
this.conf.destroy();
return;
}
this.logger.log('info', ['debug', 'resizer'], "[Resizer::applyCss] <rid:"+this.resizerId+"> will apply css.", {stretchFactors, translate});
// save stuff for quick tests (before we turn numbers into css values):
this.currentVideoSettings = {
validFor: this.conf.player.dimensions,
// videoWidth: dimensions.width,
// videoHeight: dimensions.height
}
let extraStyleString;
try {
extraStyleString = this.settings.active.sites[window.location.hostname].DOM.video.additionalCss;
} catch (e) {
// do nothing. It's ok if no special settings are defined for this site, we'll just do defaults
}
const styleArray = this.buildStyleArray('', extraStyleString)
// add remaining elements
if (stretchFactors) {
styleArray.push(`transform: translate(${translate.x}px, ${translate.y}px) scale(${stretchFactors.xFactor}, ${stretchFactors.yFactor}) !important;`);
// important — guarantees video will be properly aligned
// Note that position:absolute cannot be put here, otherwise old.reddit /w RES breaks — videos embedded
// from certain hosts will get a height: 0px. This is bad.
styleArray.push("top: 0px !important; left: 0px !important; bottom: 0px !important; right: 0px;");
// important — some websites (cough reddit redesign cough) may impose some dumb max-width and max-height
// restrictions. If site has dumb shit like 'max-width: 100%' and 'max-height: 100vh' in their CSS, that
// shit will prevent us from applying desired crop. This means we need to tell websites to fuck off with
// that crap. We know better.
styleArray.push("max-width: none !important; max-height: none !important;");
}
const styleString = `${this.buildStyleString(styleArray)}${extraStyleString || ''}`; // string returned by buildStyleString() should end with ; anyway
// build style string back
this.setStyleString(styleString);
}
setStyleString (styleString) {
this.currentCssValidFor = this.conf.player.dimensions;
const newCssString = this.prepareCss(styleString);
// inject new CSS or replace existing one
if (!this.userCss) {
this.logger.log('info', ['debug', 'resizer'], "[Resizer::setStyleString] <rid:"+this.resizerId+"> Setting new css: ", newCssString);
this.injectCss(newCssString);
this.userCss = newCssString;
} else if (newCssString !== this.userCss) {
this.logger.log('info', ['debug', 'resizer'], "[Resizer::setStyleString] <rid:"+this.resizerId+"> Replacing css.\nOld string:", this.userCss, "\nNew string:", newCssString);
// we only replace css if it
this.replaceCss(this.userCss, newCssString);
this.userCss = newCssString;
} else {
this.logger.log('info', ['debug', 'resizer'], "[Resizer::setStyleString] <rid:"+this.resizerId+"> Existing css is still valid, doing nothing.");
}
}
//#endregion
}
export default Resizer;

View File

@ -1,88 +0,0 @@
import Debug from '../../conf/Debug';
import Logger from '../Logger';
import VideoData from '../video-data/VideoData';
// računa približevanje ter računa/popravlja odmike videa
// calculates zooming and video offsets/panning
class Zoom {
//#region flags
//#endregion
//#region helper objects
conf: VideoData;
logger: Logger;
//#endregion
//#region misc data
scale: number = 1;
logScale: number = 0;
scaleStep: number = 0.1;
minScale: number = -1; // 50% (log2(0.5) = -1)
maxScale: number = 3; // 800% (log2(8) = 3)
//#endregion
constructor(videoData) {
this.conf = videoData;
this.logger = videoData.logger;
}
reset(){
this.scale = 1;
this.logScale = 0;
}
zoomStep(amount){
this.logScale += amount;
if (this.logScale <= this.minScale) {
this.logScale = this.minScale;
}
if (this.logScale >= this.maxScale) {
this.logScale = this.maxScale;
}
this.scale = Math.pow(2, this.logScale);
this.logger.log('info', 'debug', "[Zoom::zoomStep] changing zoom by", amount, ". New zoom level:", this.scale);
this.conf.resizer.toFixedAr();
this.conf.restoreAr();
this.conf.announceZoom(this.scale);
}
setZoom(scale: number, no_announce?){
this.logger.log('info', 'debug', "[Zoom::setZoom] Setting zoom to", scale, "!");
// NOTE: SCALE IS NOT LOGARITHMIC
if(scale < Math.pow(2, this.minScale)) {
scale = this.minScale;
} else if (scale > Math.pow(2, this.maxScale)) {
scale = this.maxScale;
}
this.scale = scale;
this.conf.restoreAr();
if (!no_announce) {
this.conf.announceZoom(this.scale);
}
}
applyZoom(stretchFactors){
if (!stretchFactors) {
return;
}
this.logger.log('info', 'debug', "[Zoom::setZoom] Applying zoom. Stretch factors pre:", stretchFactors, " —> scale:", this.scale);
stretchFactors.xFactor *= this.scale;
stretchFactors.yFactor *= this.scale;
this.logger.log('info', 'debug', "[Zoom::setZoom] Applying zoom. Stretch factors post:", stretchFactors);
}
}
export default Zoom;

229
src/ext/module/EventBus.ts Normal file
View File

@ -0,0 +1,229 @@
import { EventBusCommand, EventBusContext, EventBusMessage } from '@src/common/interfaces/EventBusMessage.interface';
import { IframeTunnelPayload } from '@src/common/interfaces/IframeTunnelPayload.interface';
import Comms from '@src/ext/module/comms/Comms';
import CommsClient, { CommsOrigin } from '@src/ext/module/comms/CommsClient';
import CommsServer from '@src/ext/module/comms/CommsServer';
export default class EventBus {
private name: string;
private uuid = crypto.randomUUID();
private commands: { [x: string]: EventBusCommand[]} = {};
private comms?: CommsClient | CommsServer;
private commsOrigin: CommsOrigin;
private lastExecutedCommandIds: string[] = new Array(32);
private lastExecutedCommandIndex: number = 0;
private disableTunnel: boolean = false;
private popupContext: any = {};
private iframeForwardingList: {iframe: any, fn: (action, payload, context?) => void}[] = [];
// private uiUri = window.location.href;
constructor(options?: {isUWServer?: boolean, name?: string, commsOrigin: CommsOrigin}) {
if (!options?.isUWServer) {
this.setupIframeTunnelling();
}
this.name = options?.name ?? '(unnamed EventBus)';
this.commsOrigin = options?.commsOrigin;
}
setupPopupTunnelWorkaround(context: EventBusContext): void {
this.disableTunnel = true;
this.popupContext = context;
}
//#region lifecycle
destroy() {
this.commands = null;
this.destroyIframeTunnelling();
}
//#endregion
setComms(comms: CommsClient | CommsServer) {
this.comms = comms;
}
subscribe(commandString: string, command: EventBusCommand) {
if (!this.commands[commandString]) {
this.commands[commandString] = [command];
} else {
this.commands[commandString].push(command);
}
}
subscribeMulti(commands: {[commandString: string]: EventBusCommand}, source?: any) {
for (const key in commands) {
this.subscribe(
key,
{
...commands[key],
source: source ?? commands[key].source
}
);
}
}
/**
* Removes all commands from a given source
* @param source
*/
unsubscribeAll(source: any) {
for (const commandString in this.commands) {
this.commands[commandString] = this.commands[commandString].filter(x => x.source !== source);
}
}
forwardToIframe(iframe: any, fn: (action: string, payload: any, context?: EventBusContext) => void) {
this.cancelIframeForwarding(iframe);
this.iframeForwardingList.push({iframe, fn});
}
cancelIframeForwarding(iframe: any) {
const existingForwarding = this.iframeForwardingList.findIndex((x: any) => x.iframe === iframe);
if (existingForwarding !== -1) {
this.iframeForwardingList.splice(existingForwarding, 1);
}
}
private cloneContext(context: EventBusContext = {}): EventBusContext {
return {
stopPropagation: context.stopPropagation,
origin: context.origin,
frameUrl: context.frameUrl,
visitedBusses: context.visitedBusses ? [...context.visitedBusses] : undefined,
commandId: context.commandId,
comms: context.comms ? {
forwardTo: context.comms.forwardTo,
sourceFrame: context.comms?.sourceFrame ? { ...context.comms?.sourceFrame } : undefined
} : undefined,
borderCrossings: context.borderCrossings ? { ...context.borderCrossings } : undefined
};
}
send(command: string, commandData: any, context: EventBusContext = {}) {
// context = this.cloneContext(context); // Firefox throws an error if we don't clone the context.
if (context.visitedBusses?.includes(this.uuid)) {
console.warn('this bus was already visited before. Doing nothing.');
return;
}
if (context.commandId && this.lastExecutedCommandIds.includes(context.commandId)) {
console.warn('this command was already sent');
return;
}
// we want to avoid re-assigning context.visitedBusses if possible
// in order to reduce the amount of garbage that needs to be collected.
context.visitedBusses ? context.visitedBusses.push(this.uuid) : context.visitedBusses = [this.uuid];
// execute commands we have subscriptions for
if (this.commands?.[command]) {
for (const eventBusCommand of this.commands[command]) {
eventBusCommand.function(commandData, context);
}
}
// preventing messages from flowing back to their original senders is
// CommsServer's job. EventBus does not have enough data for this decision.
// We do, however, have enough data to prevent backflow of messages that
// crossed CommsServer once already.
if (!context.commandId) {
context.commandId = crypto.randomUUID();
}
if (context.commandId) {
const i = this.lastExecutedCommandIndex++ % this.lastExecutedCommandIds.length;
this.lastExecutedCommandIds[i] = context.commandId;
}
if (!context.origin) {
context.origin = this.commsOrigin;
}
if (
this.comms
&& ( // ensure each message only enters commsServer once!
this.commsOrigin === context.origin // if these two differ, we already sent that message through Comms once,
|| this.commsOrigin === CommsOrigin.Server // CommsServer needs to forward everything, otherwise messages stop on
)
) {
try {
this.comms.sendMessage({command, config: commandData, context}, context);
} catch (e) {
if (command !== 'reload-required') {
// We shouldn't let reload-required command to trigger new reload-required commands.
this.send('reload-required', {});
}
}
};
// call forwarding functions if they exist
for (const forwarding of this.iframeForwardingList) {
forwarding.fn(
command,
commandData,
{
...context,
borderCrossings: {
...context?.borderCrossings,
}
}
);
}
// send to parent iframe
if (!this.disableTunnel && typeof window !== 'undefined') {
window.parent.postMessage(
{
action: 'uw-bus-tunnel',
payload: {command, config: commandData, context} as EventBusMessage
},
'*'
);
}
if (context?.stopPropagation) {
return;
}
}
//#endregion
//#region iframe tunnelling
private setupIframeTunnelling() {
// forward messages coming from iframe tunnels
window.addEventListener('message', this);
}
private destroyIframeTunnelling() {
window.removeEventListener('message', this);
}
/**
* Handles 'message' events (formerly handleIframeMessage)
* @param event
* @returns
*/
handleEvent(event: any) {
if (event.data?.action !== 'uw-bus-tunnel') {
return;
}
const payload = event.data.payload as EventBusMessage;
if (!payload.context?.visitedBusses) {
// this should never be visible to a real user
console.warn('Received iframe message without context. Doing nothing in order to avoid infinite loop. Event:', event);
return;
}
if (payload.context?.visitedBusses?.includes(this.uuid)) {
return;
}
this.send(payload.command, payload.config, payload.context);
}
//#endregion
}

View File

@ -1,7 +1,6 @@
import { decycle } from 'json-cyclic';
import Comms from './comms/Comms';
import BrowserDetect from '../conf/BrowserDetect';
import { browser } from 'webextension-polyfill-ts';
if (process.env.CHANNEL !== 'stable'){
console.info('Loading Logger');
@ -22,16 +21,16 @@ export const baseLoggingOptions: LoggerConfig = {
"settings": true,
"keyboard": true,
"mousemove": false,
"actionHandler": true,
"kbmHandler": true,
"comms": true,
"playerDetect": true,
"resizer": true,
"scaler": true,
"stretcher": true,
// "videoRescan": true,
// "playerRescan": true,
"videoRescan": false,
"playerRescan": false,
"arDetect": true,
"arDetect_verbose": true
"arDetect_verbose": false
},
allowBlacklistedOrigins: {
'periodicPlayerCheck': false,
@ -47,7 +46,7 @@ export interface LoggingOptions {
settings?: boolean;
keyboard?: boolean;
mousemove?: boolean;
actionHandler?: boolean;
kbmHandler?: boolean;
comms?: boolean;
playerDetect?: boolean;
resizer?: boolean;
@ -91,20 +90,26 @@ class Logger {
constructor(options?: {vuexStore?: any, uwInstance?: any}) {
this.vuexStore = options?.vuexStore;
this.uwInstance = options?.uwInstance;
chrome.storage.onChanged.addListener((changes, area) => {
this.storageChangeListener(changes, area)
});
}
static saveConfig(conf: LoggerConfig) {
console.warn('LEGACY LOGGER IS STILL BEING CALLED FROM SOMEWHERE!', new Error().stack);
if (process.env.CHANNEL === 'dev') {
console.info('Saving logger conf:', conf)
}
browser.storage.local.set( {'uwLogger': JSON.stringify(conf)});
chrome.storage.local.set( {'uwLogger': JSON.stringify(conf)});
}
static syncConfig(callback: (x) => void) {
browser.storage.onChanged.addListener( (changes, area) => {
chrome.storage.onChanged.addListener( (changes: any, area: string) => {
if (changes.uwLogger) {
const newLoggerConf = JSON.parse(changes.uwLogger.newValue)
const newLoggerConf = JSON.parse(changes.uwLogger.newValue as any)
if (process.env.CHANNEL === 'dev') {
console.info('Logger settings reloaded. New conf:', newLoggerConf);
}
@ -117,10 +122,10 @@ class Logger {
let ret;
// if (BrowserDetect.firefox) {
ret = await browser.storage.local.get('uwLogger');
ret = await chrome.storage.local.get('uwLogger');
// } else if (BrowserDetect.anyChromium) {
// ret = await new Promise( (resolve, reject) => {
// browser.storage.local.get('uwLogger', (res) => resolve(res));
// chrome.storage.local.get('uwLogger', (res) => resolve(res));
// });
// }
@ -162,12 +167,12 @@ class Logger {
if (this.conf.fileOptions === undefined) {
this.conf.fileOptions = {};
}
this.startTime = performance.now();
this.temp_disable = false;
this.stopTime = this.conf.timeout ? performance.now() + (this.conf.timeout * 1000) : undefined;
browser.storage.onChanged.addListener( (changes, area) => {
chrome.storage.onChanged.addListener( (changes: any, area: any) => {
if (process.env.CHANNEL === 'dev') {
if (!changes.uwLogger) {
// console.info('[Logger::<storage/on change> No new logger settings!');
@ -190,10 +195,37 @@ class Logger {
});
}
storageChangeListener(changes: any, area: any) {
if (!changes.uwLogger) {
return;
}
try {
this.conf = JSON.parse(changes.uwLogger.newValue);
} catch (e) {
console.warn('[uwLogger] Error while trying to parse new conf for logger:', e, '\nWe received the following changes:', changes, 'for area:', area);
}
// This code can only execute if user tried to enable or disable logging
// through the popup. In cases like this, we do not gate the console.log
// behind a check, since we _always_ want to have this feedback in response
// to an action.
console.info(
'[uwLogger] logger config changed! New configuration:',
this.conf, '\nraw changes:', changes, 'area?', area,
'\n————————————————————————————————————————————————————————————————————————\n\n\n\n\n\n\n\n\n\n\n\n-----\nLogging with new settings starts now.'
);
// initiate loger if need be
if (!this.startTime) {
this.init(this.conf);
}
}
setVuexStore(store) {
this.vuexStore = store;
}
clear() {
this.history = [];
this.startTime = performance.now();
@ -207,12 +239,12 @@ class Logger {
}
// async getSaved() {
// return Logger.getSaved();
// return Logger.getSaved();
// }
// allow syncing of start times between bg and page scripts.
// may result in negative times in the log file, but that doesn't
// may result in negative times in the log file, but that doesn't
// really matter
getStartTime() {
return this.startTime;
@ -236,8 +268,18 @@ class Logger {
// return logfileStr;
// }
getFileLogJSONString() {
let site;
// NOTE: no more window object on UWServer side of things!
// (or rather, we could get it, but we don't care enough to get it in this instance)
try {
site = window && window.location;
} catch {
site = 'uw-bg';
}
return {
site: window && window.location,
site,
log: JSON.stringify(this.history),
}
}
@ -254,7 +296,7 @@ class Logger {
}
// this should be used mostly in background page instance of logger, btw
//
//
addToGlobalHistory(key, log) {
this.globalHistory[key] = log;
this.log('info', 'debug', 'Added log for', key, 'to global history. Current history:', this.globalHistory);
@ -326,8 +368,8 @@ class Logger {
stackInfo.stack.trace.splice(i);
break;
}
}
}
return stackInfo;
}
@ -379,14 +421,14 @@ class Logger {
if (this.isBlacklistedOrigin(stackInfo)) {
return false;
}
}
// if either of these two is true, we allow logging to happen (forbidden origins were checked above)
return (this.canLogFile(component) || this.canLogConsole(component) || stackInfo.exitLogs);
}
canLogFile(component) {
if (!this.conf.fileOptions.enabled || this.temp_disable) {
if (!this.conf?.fileOptions?.enabled || this.temp_disable) {
return false;
}
if (Array.isArray(component) && component.length ) {
@ -416,7 +458,7 @@ class Logger {
return this.conf.logAll;
}
logToFile(message, stackInfo) {
let ts = performance.now();
if (ts <= this.history[this.history.length - 1]) {
@ -433,16 +475,16 @@ class Logger {
logToConsole(level, message, stackInfo) {
try {
switch (level) {
case 'error':
case 'error':
console.error(...message, {stack: stackInfo});
break;
case 'warn':
console.warn(...message, {stack: stackInfo});
break;
case 'info':
case 'info':
console.info(...message, {stack: stackInfo});
break;
default:
default:
console.log(...message, {stack: stackInfo});
}
} catch (e) {
@ -480,7 +522,7 @@ class Logger {
if (this.isBlacklistedOrigin(stackInfo)) {
return;
}
if (this.conf.fileOptions?.enabled) {
if (this.canLogFile(component) || stackInfo.exitLogs) {
this.logToFile(message, stackInfo);
@ -513,7 +555,7 @@ class Logger {
message: "-------------------------------------- CAHEN --------------------------------------"
});
// find the spot for the half second mark. In this case, we don't really particularly care whether timestamps
// find the spot for the half second mark. In this case, we don't really particularly care whether timestamps
// are duped due to cahen warnings
while (this.history[i--].ts > halfSecondMark) {}
this.history.push({
@ -561,7 +603,7 @@ class Logger {
Comms.sendMessage({cmd: 'show-logger', forwardToSameFramePort: true, port: 'content-ui-port'});
let exportObject;
let exportObject;
try {
exportObject = {
pageLogs: decycle(this.history),
@ -599,7 +641,7 @@ class Logger {
console.info('[info] vuex store present. Parsing logs.');
let exportObject;
let exportObject;
try {
exportObject = {
pageLogs: decycle(this.history),

View File

@ -0,0 +1,74 @@
export default class UWGlobals {
constructor() {
this.videos = [];
this.busSubscriptions = [];
this.actionSubscriptions = {};
this.bus = {
sendMessage: (action, config) => this.propagateMessages(action, config),
subscribe: this.subscribeToAny,
subscribeToAction: this.subscribeToAction
}
}
getNewVideoID() {
let random;
while (true) {
// 4-digit [a-z0-9] string. Should be unique per page on first try
random = (Math.random() * 1679616).toFixed().toString(36);
if (this.videos.findIndex(x => x.vdid === random) === -1) {
return random;
}
}
}
addVideo(video) {
// get video ID
const id = this.getNewVideoID();
video.vdid = id;
this.videos.push(video);
}
getVideo(id) {
return this.videos.find(x => x.vdid === id);
}
importSubscriptionsFromCommsHandlers(commands) {
for (const action in commands) {
for (const command of commands[action]) {
this.subscribeToAction(action, command);
}
}
}
subscribeToAction(action, callback) {
if (!this.actionSubscriptions[action]) {
this.actionSubscriptions[action] = [];
}
this.actionSubscriptions[action].push(callback);
}
subscribeToAny(callback) {
this.busSubscriptions.push(callback);
}
propagateMessages(action, config) {
if (this.busSubscriptions) {
for (const subscription of this.busSubscriptions) {
subscription(action, config);
}
}
if (this.actionSubscriptions && this.actionSubscriptions[action]) {
for (const subscription of this.actionSubscriptions[action]) {
subscription(config);
}
}
}
destroy() {
// todo: implement
}
}

1439
src/ext/module/aard/Aard.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,390 @@
import { Aard } from './Aard';
import { AardLegacy } from './AardLegacy';
import { AardPerformanceData } from './AardTimers';
import { FallbackCanvas } from './gl/FallbackCanvas';
export class AardDebugUi {
aard: any;
uiAnchorElement: HTMLDivElement;
pauseOnArCheck: boolean = false;
uiVisibility: any = {};
constructor(aard: any) {
this.aard = aard;
this.uiVisibility = {
detectionDetails: aard.settings.active.ui.dev.aardDebugOverlay.showDetectionDetails
};
(window as any).ultrawidify_uw_aard_debug_tools = {
enableStopOnChange: () => this.changePauseOnCheck(true),
disableStopOnChange: () => this.changePauseOnCheck(false),
resumeVideo: () => this.resumeVideo(),
step: () => this.aard.step()
}
}
initContainer() {
const div = document.createElement('div');
div.id = 'uw-aard-debug-ui-container';
div.innerHTML = `
<div style="
position: fixed; top: 0; left: 0; width: 100vw; height: 100dvh; display: flex; flex-direction: column; pointer-events: none; z-index: 9999; font-size: 16px; font-family: 'Overpass Mono', monospace;
">
<div style="width: 100%; display: flex; flex-direction: row; justify-content: space-between; backdrop-filter: blur(0.5rem) brightness(0.5);">
<div style="padding: 1rem; color: #fff">
<h1>Aard debug overlay</h1>
</div>
<div style="pointer-events: all; display: flex; flex-direction: column; margin-right: 1rem;">
<button id="uw-aard-debug_show-detection-details">Show det. details</button>
<button id="uw-aard-debug_hide-detection-details">Hide det. details</button>
</div>
<style>
#uw-aard-debug_performance-container #uw-aard-debug_performance-popup {
display: none;
}
#uw-aard-debug_performance-container:hover #uw-aard-debug_performance-popup {
display: block;
position: fixed;
left: 0;
top: 3rem;
width: 100vw;
color: #aaa;
background-color: #000;
border: 2px solid #fa6;
padding: 16px;
padding-right: 32px;
box-sizing: border-box;
backdrop-filter: blur(1rem) brightness(0.5);
pointer-events: none;
font-size: 12px;
}
</style>
<div id="uw-aard-debug_performance-container" style="flex-grow: 1; position: relative; pointer-events: auto;">
<div id="uw-aard-debug_performance-mini" style="width: 100%; color: #aaa; display: flex; flex-direction: column; font-size: 12px;"></div>
<div id="uw-aard-debug_performance-popup" style="position: fixed; top: 3rem; z-index: 3000;">
</div>
</div>
<div style="pointer-events: all">
<button id="uw-aard-debug-ui_close-overlay">Close overlay</button>
</div>
</div>
<div id="uw-aard-debug-ui_body" style="display: flex; flex-direction: row; width: 100%; margin-top: 8rem;">
<div style="">
<div id="uw-aard-debug_aard-sample-canvas" style="min-width: 640px"></div>
<div style="background: black; color: #fff"; font-size: 24px;">AARD IN</div>
<div style="pointer-events: all">
<button id="uw-aard-debug-ui_enable-stop-on-change" style="">Pause video on aspect ratio change</button>
<button id="uw-aard-debug-ui_disable-stop-on-change" style="display: none">Stop pausing video on aspect ratio change</button>
<button id="uw-aard-debug-ui_resume-video" >Resume video</button>
<button id="uw-aard-debug-ui_enable-step" >Run ARD once</button>
<button id="uw-aard-debug-ui_enable-step-nocache" >Run ARD (bypass cache)</button>
</div>
</div>
<div style="flex-grow: 1"></div>
<div>
<div style="background: black; color: #ccc;">
<div style="font-size: 24px; padding: 1rem;">
Debug results:
</div>
<pre id="uw-aard-results"></pre>
</div>
</div>
<div style="width: 1920px; border: 2px dotted #142; margin-right:2rem;">
<div id="uw-aard-debug_aard-output" style="zoom: 3; image-rendering: pixelated;"></div>
<div style="background: black; color: #fff; font-size: 24px;">AARD RESULT</div>
</div>
</div>
</div>
`;
document.body.appendChild(div);
this.uiAnchorElement = div;
document.getElementById('uw-aard-debug-ui_enable-stop-on-change').onclick = () => this.changePauseOnCheck(true);
document.getElementById('uw-aard-debug-ui_disable-stop-on-change').onclick = () => this.changePauseOnCheck(false);
document.getElementById('uw-aard-debug-ui_resume-video').onclick = () => this.resumeVideo();
document.getElementById('uw-aard-debug-ui_enable-step').onclick = () => this.aard.step();
document.getElementById('uw-aard-debug-ui_enable-step-nocache').onclick = () => this.aard.step({noCache: true});
document.getElementById('uw-aard-debug-ui_close-overlay').onclick = () => (this.aard as any).hideDebugCanvas();
document.getElementById('uw-aard-debug_show-detection-details').onclick = () => {this.uiVisibility.detectionDetails = true; this.setOverlayVisibility();};
document.getElementById('uw-aard-debug_hide-detection-details').onclick = () => {this.uiVisibility.detectionDetails = false; this.setOverlayVisibility();};
this.setOverlayVisibility();
}
changePauseOnCheck(pauseOnChange: boolean) {
this.pauseOnArCheck = pauseOnChange;
document.getElementById("uw-aard-debug-ui_enable-stop-on-change").style.display = pauseOnChange ? "none" : "";
document.getElementById("uw-aard-debug-ui_disable-stop-on-change").style.display = pauseOnChange ? "" : "none";
}
destroyContainer() {
this.uiAnchorElement.remove();
}
attachCanvases(sample: HTMLCanvasElement, debug: HTMLCanvasElement) {
const sampleCanvasParent = document.getElementById('uw-aard-debug_aard-sample-canvas');
sampleCanvasParent.appendChild(sample);
const debugCanvasParent = document.getElementById('uw-aard-debug_aard-output');
debugCanvasParent.appendChild(debug);
}
resumeVideo() {
(this.aard as any).video.play();
this.aard.start();
}
private updatePerformanceResults() {
const previewDiv = document.getElementById('uw-aard-debug_performance-mini');
const popupDiv = document.getElementById('uw-aard-debug_performance-popup');
const previewContent = `
<div style="display: flex; flex-direction: row">
<div style="width: 120px">Current:</div>
<div style="flex-grow: 1;">${this.generateMiniGraphBar(this.aard.timer.current)}</div>
</div>
<div style="display: flex; flex-direction: row">
<div style="width: 120px">Average:</div>
<div style="flex-grow: 1">${this.generateMiniGraphBar(this.aard.timer.average)}</div>
</div>
<div style="display: flex; flex-direction: row">
<div style="width: 120px">Last chg.:</div>
<div></div>
<div style="flex-grow: 1">${this.generateMiniGraphBar(this.aard.timer.lastChange)}</div>
</div>
`;
const popupContent = `
<h2 style="color: #fa6; margin-bottom: 1rem">Detailed performance analysis:</h2>
<div>
<span style="color: #fff">About:</span><br/>
Aard version: <span style="color: #fa6">${this.aard instanceof AardLegacy ? 'legacy' : this.aard instanceof Aard ? 'experimental' : 'unknown'}</span> <small>(instanceof AardLegacy? ${this.aard instanceof AardLegacy}, Aard? ${this.aard instanceof Aard})</small><br/>
canvas type: <span style="color: #fa6">${!this.aard.canvasStore.main ? '<canvas not initialized>' : this.aard.canvasStore.main instanceof FallbackCanvas ? '2dCanvas' : 'webgl'}</span> |
legacy default: <span style="color: #fa6">${this.aard.settings.active.aardLegacy.aardType}</span>,
experimental default: <span style="color: #fa6">${this.aard.settings.active.aard.aardType}</span>
</div>
<br/>
<div style="width: 100%; display: flex; flex-direction: column">
<div style="margin-bottom: 1rem;">
${this.generateRawTimes(this.aard.timer.current)}
</div>
<div style="color: #fff">Stage times (not cumulative):</div>
<div style="display: flex; flex-direction: row; width: 100%; height: 150px">
<div style="width: 160px; text-align: right; padding-right: 4px;">Current:</div>
<div style="flex-grow: 1;">${this.generateMiniGraphBar(this.aard.timer.current, true)}</div>
</div>
<div style="margin-bottom: 1rem;">
${this.generateRawTimes(this.aard.timer.average)}
</div>
<div style="color: #fff">Stage times (not cumulative):</div>
<div style="display: flex; flex-direction: row; width: 100%; height: 150px">
<div style="width: 160px; text-align: right; padding-right: 4px;">Average:</div>
<div style="flex-grow: 1;">${this.generateMiniGraphBar(this.aard.timer.average, true)}</div>
</div>
<div style="margin-bottom: 1rem;">
${this.generateRawTimes(this.aard.timer.lastChange)}
</div>
<div style="display: flex; flex-direction: row; width: 100%; height: 150px">
<div style="width: 160px; text-align: right; padding-right: 4px;">Last change:</div>
<div style="flex-grow: 1;">${this.generateMiniGraphBar(this.aard.timer.lastChange, true)}</div>
</div>
<!-- <pre>${JSON.stringify({current: this.aard.timer.current, average: this.aard.timer.average, lastChange: this.aard.timer.lastChange}, null, 2)}</pre> -->
</div>
`
previewDiv.innerHTML = previewContent;
popupDiv.innerHTML = popupContent;
}
private getBarLabel(width: number, leftOffset: number, topOffset: number, label: string, detailed: boolean, extraStyles?: string) {
if (!detailed) {
return '';
}
let offsets: string;
let text: string = '';
if (leftOffset + width < 80) {
// at the end of the bar
offsets = `left: ${leftOffset + width}%;`;
} else {
if (width < 15 && leftOffset < 100) {
// before the bar
offsets = `right: ${100 - leftOffset}%;`;
text = 'color: #fff;';
} else {
// inside the bar, aligned to right
offsets = `right: ${Math.max(100 - (leftOffset + width), 0)}%;`
}
}
return `
<div style="
position: absolute;
${offsets} top: ${topOffset}px;
padding-left: 0.5rem; padding-right: 0.5rem;đ
${text}
text-shadow: 0 0 2px #000, 0 0 2px #000; 0 0 2px #000; 0 0 2px #000;
${extraStyles}
">
${label}
</div>
`;
}
private generateRawTimes(timer) {
return `
<span style="color: #fff">Raw times (cumulative; -1 = test was skipped):</span><br/>
draw <span style="color: #fa6; margin-right: 0.5rem;">${timer.draw.toFixed(2)}ms</span>
get data <span style="color: #fa6; margin-right: 0.5rem;">${timer.getImage.toFixed(2)}ms</span>
black lv. <span style="color: #fa6; margin-right: 0.5rem;">${timer.fastBlackLevel.toFixed(2)}ms</span>
guard/image <span style="color: #fa6; margin-right: 0.5rem;">${timer.guardLine.toFixed(3)}ms</span>
edge <span style="color: #fa6; margin-right: 0.5rem;">${timer.edgeScan.toFixed(2)}ms</span>
gradient <span style="color: #fa6; margin-right: 0.5rem;">${timer.gradient.toFixed(3)}ms</span>
post <span style="color: #fa6; margin-right: 0.5rem;">${timer.scanResults.toFixed(2)}ms</span>
subtitle <span style="color: #fa6; margin-right: 0.5rem;">${timer.subtitleScan.toFixed(2)}ms</span>
`;
}
private generateMiniGraphBar(perf: AardPerformanceData, detailed: boolean = false) {
if (!perf) {
return `
n/a
`;
}
let total = 0;
const draw = Math.max(perf.draw, 0);
total += draw;
const getImageStart = draw;
const getImage = Math.max(perf.getImage - total, 0);
total += getImage;
const fastBlackLevelStart = getImageStart + getImage;
const fastBlackLevel = Math.max(perf.fastBlackLevel - total, 0);
total += fastBlackLevel;
const guardLineStart = fastBlackLevelStart + fastBlackLevel;
const guardLine = (perf.guardLine !== undefined && perf.guardLine !== -1) ? Math.max(perf.guardLine - total, 0) : 0;
total += guardLine;
const edgeScanStart = guardLineStart + guardLine;
const edgeScan = (perf.edgeScan !== undefined && perf.edgeScan !== -1) ? Math.max(perf.edgeScan - total, 0) : 0;
total += edgeScan;
const gradientStart = edgeScanStart + edgeScan;
const gradient = (perf.gradient !== undefined && perf.gradient !== -1) ? Math.max(perf.gradient - total, 0) : 0;
total += gradient;
const scanResultsStart = gradientStart + gradient;
const scanResults = (perf.scanResults !== undefined && perf.scanResults !== -1) ? Math.max(perf.scanResults - total, 0) : 0;
total += scanResults;
const subtitleScanStart = scanResultsStart + scanResults;
const subtitleScan = Math.max(perf.subtitleScan - total, 0);
total += subtitleScan;
return `
<div style="width: calc(100% - 2rem); position: relative; text-align: right;">
${detailed ? '' : `${total.toFixed()} ms`}
<div style="position: absolute; top: 0; left: 0; width: ${total}%; background: #fff; height: 2px;"></div>
${detailed ? `<div style="position: absolute; top: 0; left: ${total}%; height: 100px; width: 1px; border-left: 1px dotted rgba(255,255,255,0.5)"></div> ` : ''}
<div style="position: absolute; top: ${detailed ? '2px' : '2px'}; left: 0; min-width: 1px; width: ${draw}%; background: #007; height: 12px;"></div>
${this.getBarLabel(draw, 0, 2, `draw: ${draw.toFixed(2)} ms`, detailed)}
<div style="position: absolute; top: ${detailed ? '14px' : '2px'}; left: ${getImageStart}%; min-width: 1px; width: ${getImage}%; background: #00a; height: 12px;"></div>
${this.getBarLabel(getImage, getImageStart, 14, `get data: ${getImage.toFixed(2)} ms`, detailed)}
<div style="position: absolute; top: ${detailed ? '26px' : '2px'}; left: ${fastBlackLevelStart}%; min-width: 1px; width: ${fastBlackLevel}%; background: #03a; height: 12px;"></div>
${this.getBarLabel(fastBlackLevel, fastBlackLevelStart, 26, `black level: ${fastBlackLevel.toFixed(2)} ms`, detailed)};
<div style="position: absolute; top: ${detailed ? '38px' : '2px'}; left: ${guardLineStart}%; min-width: 1px; width: ${guardLine}%; background: #0f3; height: 12px;"></div>
${this.getBarLabel(guardLine, guardLineStart, 38, `guard/image line: ${guardLine.toFixed(2)} ms`, detailed)}
<div style="position: absolute; top: ${detailed ? '50px' : '2px'}; left: ${edgeScanStart}%; min-width: 1px; width: ${edgeScan}%; background: #f51; height: 12px;"></div>
${this.getBarLabel(edgeScan, edgeScanStart, 50, `edge scan (/w validation): ${edgeScan.toFixed(2)} ms`, detailed)}
<div style="position: absolute; top: ${detailed ? '62px' : '2px'}; left: ${gradientStart}%; min-width: 1px; width: ${gradient}%; background: #fa6; height: 12px;"></div>
${this.getBarLabel(gradient, gradientStart, 62, `gradient: ${gradient.toFixed(2)} ms`, detailed)}
<div style="position: absolute; top: ${detailed ? '74px' : '2px'}; left: ${scanResultsStart}%; min-width: 1px; width: ${scanResults}%; background: #80f; height: 12px;"></div>
${this.getBarLabel(scanResults, scanResultsStart, 74, `scan results processing: ${scanResults.toFixed(2)} ms`, detailed)}
<div style="position: absolute; top: ${detailed ? '86px' : '2px'}; left: ${subtitleScanStart}%; min-width: 1px; width: ${subtitleScan}%; background: rgba(234, 204, 84, 1); height: 12px;"></div>
${this.getBarLabel(subtitleScan, subtitleScanStart, 86, `subtitle scan: ${subtitleScan.toFixed(2)} ms`, detailed)}
${this.getBarLabel(0, subtitleScan + subtitleScanStart, 98, `total: ${total.toFixed(2)} ms`, detailed, 'color: #fff;')}
<!-- 60/30 fps markers -->
<div style="position: absolute; top: ${detailed ? '-12px' : '0'}; left: 16.666%; width: 1px; border-left: 1px dashed #4f9; height: ${detailed ? '112px' : '12px'}; padding-left: 2px; background-color: rgba(0,0,0,0.5); z-index: ${detailed ? '5' : '2'}000;">60fps</div>
<div style="position: absolute; top: ${detailed ? '-12px' : '0'}; left: 33.333%; width: 1px; border-left: 1px dashed #ff0; height: ${detailed ? '112px' : '12px'}; padding-left: 2px; background-color: #000; z-index: ${detailed ? '5' : '2'}000;">30fps</div>
</div>
`;
}
_lastAr: undefined;
updateTestResults(testResults, timers) {
this.updatePerformanceResults();
if (testResults.aspectRatioUpdated && this.pauseOnArCheck) {
(this.aard as any).video.pause();
this.aard.stop();
}
const resultsDiv = document.getElementById('uw-aard-results');
const ar = testResults.activeAspectRatio.toFixed(3);
let out = `
LAST STAGE: ${testResults.lastStage} | black level: ${testResults.blackLevel}, threshold: ${testResults.blackThreshold}
-- ASPECT RATIO
Active: ${ar}, changed since last check? ${testResults.aspectRatioUpdated} letterbox width: ${testResults.letterboxWidth} offset ${testResults.letterboxOffset}<br/>
<sup>(last: ${this._lastAr})</sup>
${
timers ?
`
Paused until? ${Date.now() < timers?.pauseUntil ? ((timers?.pauseUntil - Date.now()) / 1000) + 's' : 'not paused'};
<sup>now: ${Date.now()}ms; until:${timers?.pauseUntil}ms; diff: ${(+timers?.pauseUntil - Date.now())} </sup>
` :
`- timers are missing -`
}
image in black level probe (aka "not letterbox"): ${testResults.notLetterbox}
`;
this._lastAr = ar;
resultsDiv.innerHTML = out;
}
private setOverlayVisibility() {
document.getElementById('uw-aard-debug-ui_body').style.display = this.uiVisibility.detectionDetails ? 'flex' : 'none';
document.getElementById('uw-aard-debug_hide-detection-details').style.display = this.uiVisibility.detectionDetails ? '' : 'none';
document.getElementById('uw-aard-debug_show-detection-details').style.display = this.uiVisibility.detectionDetails ? 'none' : '';
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,135 @@
export interface AardPerformanceData {
start: number;
draw: number;
getImage: number;
fastBlackLevel: number;
// optional ones are only available in legacy aard
guardLine?: number; // actually times both guard line and image line checks
edgeScan?: number; // includes validation step
gradient?: number;
scanResults?: number;
subtitleScan: number;
}
export class AardTimer {
private currentIndex: number = -1;
private aardPerformanceDataBuffer: AardPerformanceData[];
current: AardPerformanceData;
previous: AardPerformanceData;
average: AardPerformanceData;
lastChange: AardPerformanceData;
constructor() {
// we need to deep clone, otherwise all buffer objects will point to the same object
// (this makes calculating averages impossible)
this.aardPerformanceDataBuffer = JSON.parse(JSON.stringify(new Array<AardPerformanceData>(64).fill(this.getEmptyMeasurement())));
this.current = this.aardPerformanceDataBuffer[0];
this.previous = undefined;
this.lastChange = this.getEmptyMeasurement();
this.average = this.getEmptyMeasurement();
}
private getEmptyMeasurement(): AardPerformanceData {
return {
start: -1,
draw: -1,
getImage: -1,
fastBlackLevel: -1,
guardLine: -1,
edgeScan: -1,
gradient: -1,
scanResults: -1,
subtitleScan: -1,
}
};
private clearMeasurement(index: number) {
this.aardPerformanceDataBuffer[index].draw = -1;
this.aardPerformanceDataBuffer[index].getImage = -1;
this.aardPerformanceDataBuffer[index].fastBlackLevel = -1;
this.aardPerformanceDataBuffer[index].guardLine = -1;
this.aardPerformanceDataBuffer[index].edgeScan = -1;
this.aardPerformanceDataBuffer[index].gradient = -1;
this.aardPerformanceDataBuffer[index].scanResults = -1;
this.aardPerformanceDataBuffer[index].subtitleScan = -1;
}
next() {
// go to next buffer position;
this.currentIndex = (this.currentIndex + 1) % this.aardPerformanceDataBuffer.length;
this.previous = this.current;
this.clearMeasurement(this.currentIndex);
// TODO: reset values
this.current = this.aardPerformanceDataBuffer[this.currentIndex];
}
arChanged() { // copy numbers over
this.lastChange.draw = this.current.draw;
this.lastChange.getImage = this.current.getImage;
this.lastChange.fastBlackLevel = this.current.fastBlackLevel;
this.lastChange.guardLine = this.current.guardLine;
this.lastChange.edgeScan = this.current.edgeScan;
this.lastChange.gradient = this.current.gradient;
this.lastChange.scanResults = this.current.scanResults;
this.lastChange.subtitleScan = this.current.scanResults;
}
getAverage() {
for (let i = 0; i < this.aardPerformanceDataBuffer.length; i++) {
const sample = this.aardPerformanceDataBuffer[i];
if (sample.draw !== -1) {
this.average.draw += sample.draw;
}
if (sample.getImage !== -1) {
this.average.getImage += sample.getImage;
}
if (sample.fastBlackLevel !== -1 && this.average.fastBlackLevel !== null) {
this.average.fastBlackLevel += sample.fastBlackLevel;
} else {
this.average.fastBlackLevel = null;
}
if (sample.guardLine !== -1 && this.average.guardLine !== null) {
this.average.guardLine += sample.guardLine;
} else {
this.average.guardLine = null;
}
if (sample.edgeScan !== -1 && this.average.edgeScan !== null) {
this.average.edgeScan += sample.edgeScan;
} else {
this.average.edgeScan = null;
}
if (sample.gradient !== -1 && this.average.gradient !== null) {
this.average.gradient += sample.gradient;
} else {
this.average.edgeScan = null;
}
if (sample.scanResults !== -1 && this.average.scanResults !== null) {
this.average.scanResults += sample.scanResults;
} else {
this.average.scanResults = null;
}
if (sample.subtitleScan !== -1 && this.average.subtitleScan !== null) {
this.average.subtitleScan += sample.subtitleScan;
} else {
this.average.subtitleScan = null;
}
}
this.average.draw /= this.aardPerformanceDataBuffer.length;
this.average.getImage /= this.aardPerformanceDataBuffer.length;
this.average.fastBlackLevel /= this.aardPerformanceDataBuffer.length;
this.average.guardLine = this.average.guardLine === null ? -1 : (this.average.guardLine / this.aardPerformanceDataBuffer.length);
this.average.edgeScan = this.average.edgeScan === null ? -1 : (this.average.guardLine / this.aardPerformanceDataBuffer.length);
this.average.gradient = this.average.gradient === null ? -1 : (this.average.guardLine / this.aardPerformanceDataBuffer.length);
this.average.scanResults = this.average.scanResults === null ? -1 : (this.average.guardLine / this.aardPerformanceDataBuffer.length);
this.average.subtitleScan = this.average.subtitleScan === null ? -1 : (this.average.subtitleScan /this.aardPerformanceDataBuffer.length);
}
}

View File

@ -0,0 +1,5 @@
export enum AardUncertainReason {
InsufficientEdgeDetectionQuality = 1,
LetterboxNotCenteredEnough = 2,
TopAndBottomRowMismatch = 3,
}

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