From ed8ece2ad4026682f27d341f6004ada562b91e4c Mon Sep 17 00:00:00 2001 From: Heikki Hellgren Date: Tue, 6 Aug 2024 10:09:58 +0300 Subject: [PATCH] feat: add support for Firefox --- .gitignore | 5 +- README.md | 35 +- {images => chrome/images}/icon_1024.png | Bin {images => chrome/images}/icon_128.png | Bin {images => chrome/images}/icon_16.png | Bin {images => chrome/images}/icon_32.png | Bin {images => chrome/images}/icon_64.png | Bin manifest.json => chrome/manifest.json | 0 options.html => chrome/options.html | 0 options.js => chrome/options.js | 4 +- yt.js => chrome/yt.js | 21 +- yt_auto_pause.js => chrome/yt_auto_pause.js | 0 create_package.sh | 11 - firefox/images/icon_1024.png | Bin 0 -> 29139 bytes firefox/images/icon_128.png | Bin 0 -> 8036 bytes firefox/images/icon_16.png | Bin 0 -> 2318 bytes firefox/images/icon_32.png | Bin 0 -> 3221 bytes firefox/images/icon_64.png | Bin 0 -> 4701 bytes firefox/manifest.json | 84 +++++ firefox/options.html | 139 +++++++ firefox/options.js | 118 ++++++ firefox/yt.js | 393 ++++++++++++++++++++ firefox/yt_auto_pause.js | 245 ++++++++++++ package.json | 38 ++ 24 files changed, 1061 insertions(+), 32 deletions(-) rename {images => chrome/images}/icon_1024.png (100%) rename {images => chrome/images}/icon_128.png (100%) rename {images => chrome/images}/icon_16.png (100%) rename {images => chrome/images}/icon_32.png (100%) rename {images => chrome/images}/icon_64.png (100%) rename manifest.json => chrome/manifest.json (100%) rename options.html => chrome/options.html (100%) rename options.js => chrome/options.js (96%) rename yt.js => chrome/yt.js (95%) rename yt_auto_pause.js => chrome/yt_auto_pause.js (100%) delete mode 100755 create_package.sh create mode 100644 firefox/images/icon_1024.png create mode 100644 firefox/images/icon_128.png create mode 100644 firefox/images/icon_16.png create mode 100644 firefox/images/icon_32.png create mode 100644 firefox/images/icon_64.png create mode 100644 firefox/manifest.json create mode 100644 firefox/options.html create mode 100644 firefox/options.js create mode 100644 firefox/yt.js create mode 100644 firefox/yt_auto_pause.js create mode 100644 package.json diff --git a/.gitignore b/.gitignore index d337f31..0cc11f5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .idea .DS_Store Archive.zip -images/store -package.zip \ No newline at end of file +**/images/store +web-ext-artifacts +node_modules \ No newline at end of file diff --git a/README.md b/README.md index e3dc0a5..ffcf90e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ ![Youtube Auto Pause](yt_auto_pause.png) -This is a Chrome extension that pauses Youtube videos when losing the tab/window focus by +This is a Chrome & Firefox extension that pauses Youtube videos when losing the tab/window focus by sending events to the player. Resumes the playback once the Youtube tab/window is back in focus. Also listens for computer lock events and when the video goes out of viewport @@ -10,18 +10,28 @@ Features some useful keyboard shortcuts to control videos in the window. ## Installing -**From chrome web store** +**From web store** -https://chrome.google.com/webstore/detail/pbehcnkdmffkllmlfjpblpjhflnafioo/ +Chrome: https://chrome.google.com/webstore/detail/pbehcnkdmffkllmlfjpblpjhflnafioo/ +Firefox: https://addons.mozilla.org/en-US/firefox/addon/youtube-auto-pause/ -**Manually** +**Manually (chrome)** 1. Clone the repository 2. Start chrome -3. Go to chrome://extensions +3. Go to `chrome://extensions` 4. Enable developer mode 5. Click on "Load unpacked" -6. Select the cloned folder +6. Select the `chrome` directory in the repository + +**Manually (firefox)** + +1. Clone the repository +2. Start firefox +3. Go to `about:debugging` +4. Click on "This Firefox" +5. Click on "Load Temporary Add-on" +6. Select the `firefox` directory in the repository ## Supported services @@ -37,6 +47,15 @@ Please feel free to contribute with pull requests or by creating issues. In case the extension does not work, please also list all other extensions you have enabled as this might conflict with other extensions. -## TODO +### Running and developing + +To run the extension, either use `npm run start:chrome` or `npm run start:firefox` to start +specific browser. This will open a new browser window with the extension enabled. + +If you want to debug the extension further, use `npm run debug:chrome` and `npm run debug:firefox` +respectively. This will open the browser's developer tools that you can use to debug the extension. + +### Building -- Allow selecting video services +Run `npm install` to fetch necessary dependencies. Then run `npm run build` to build +the extension packages under `web-ext-artifacts`. diff --git a/images/icon_1024.png b/chrome/images/icon_1024.png similarity index 100% rename from images/icon_1024.png rename to chrome/images/icon_1024.png diff --git a/images/icon_128.png b/chrome/images/icon_128.png similarity index 100% rename from images/icon_128.png rename to chrome/images/icon_128.png diff --git a/images/icon_16.png b/chrome/images/icon_16.png similarity index 100% rename from images/icon_16.png rename to chrome/images/icon_16.png diff --git a/images/icon_32.png b/chrome/images/icon_32.png similarity index 100% rename from images/icon_32.png rename to chrome/images/icon_32.png diff --git a/images/icon_64.png b/chrome/images/icon_64.png similarity index 100% rename from images/icon_64.png rename to chrome/images/icon_64.png diff --git a/manifest.json b/chrome/manifest.json similarity index 100% rename from manifest.json rename to chrome/manifest.json diff --git a/options.html b/chrome/options.html similarity index 100% rename from options.html rename to chrome/options.html diff --git a/options.js b/chrome/options.js similarity index 96% rename from options.js rename to chrome/options.js index 8498209..397b42c 100644 --- a/options.js +++ b/chrome/options.js @@ -76,7 +76,7 @@ for (host of hosts) { label.appendChild(checkbox); const span = document.createElement("span"); span.className = "label-text"; - span.innerHTML = formatHostName(host); + span.textContent = formatHostName(host); label.appendChild(span); hostsDiv.appendChild(label); checkbox.addEventListener("change", save_options); @@ -84,7 +84,7 @@ for (host of hosts) { // Show version in the options window const version = document.getElementById("version"); -version.innerHTML = "v" + chrome.runtime.getManifest().version; +version.textContent = "v" + chrome.runtime.getManifest().version; // Restore options on load and when they change in the store document.addEventListener("DOMContentLoaded", restore_options); diff --git a/yt.js b/chrome/yt.js similarity index 95% rename from yt.js rename to chrome/yt.js index 8759ca7..333f4f3 100644 --- a/yt.js +++ b/chrome/yt.js @@ -20,8 +20,8 @@ let options = { debugMode: false, }; -var hosts = chrome.runtime.getManifest().host_permissions; -for (var host of hosts) { +const hosts = chrome.runtime.getManifest().host_permissions; +for (const host of hosts) { options[host] = true; } @@ -75,7 +75,7 @@ function isEnabledForTab(tab) { }); if (optionKey) { - return options[optionKey]; + return !!options[optionKey]; } return false; @@ -144,7 +144,7 @@ function toggle_mute(tab) { } // Listen options changes -chrome.storage.onChanged.addListener(async function (changes, namespace) { +chrome.storage.onChanged.addListener(async function (changes) { enabledTabs = []; for (const key in changes) { debugLog( @@ -154,14 +154,17 @@ chrome.storage.onChanged.addListener(async function (changes, namespace) { } if ("disabled" in changes) { - refresh_settings(); - const tabs = await chrome.tabs.query({ active: true }); + const tabs = await browser.tabs.query({ active: true }); + if (!options.disabled) { + debugLog(`Extension enabled, resuming active tabs`); + } else { + debugLog(`Extension disabled, stopping active tabs`); + } + for (let i = 0; i < tabs.length; i++) { - if (options.disabled === true) { - debugLog(`Extension enabled, resuming active tabs`); + if (!options.disabled) { resume(tabs[i]); } else { - debugLog(`Extension disabled, stopping active tabs`); stop(tabs[i]); } } diff --git a/yt_auto_pause.js b/chrome/yt_auto_pause.js similarity index 100% rename from yt_auto_pause.js rename to chrome/yt_auto_pause.js diff --git a/create_package.sh b/create_package.sh deleted file mode 100755 index 95e8def..0000000 --- a/create_package.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/sh - -zip -r package.zip . \ - -x ".*" \ - -x "*/.*" \ - -x "create_package.sh" \ - -x "package.zip" \ - -x ".DS_Store" \ - -x "README.md" \ - -x "LICENSE" \ - -x "yt_auto_pause.png" \ No newline at end of file diff --git a/firefox/images/icon_1024.png b/firefox/images/icon_1024.png new file mode 100644 index 0000000000000000000000000000000000000000..a9ff636126d4de6c9346031102a1c64f139de049 GIT binary patch literal 29139 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 literal 0 HcmV?d00001 diff --git a/firefox/images/icon_128.png b/firefox/images/icon_128.png new file mode 100644 index 0000000000000000000000000000000000000000..426e92ae3694e854a44c9a7ebaebfbef491473d3 GIT binary patch literal 8036 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 literal 0 HcmV?d00001 diff --git a/firefox/images/icon_32.png b/firefox/images/icon_32.png new file mode 100644 index 0000000000000000000000000000000000000000..292c3fbccaeeeeecc5fb8144c44b2a9e79e84b96 GIT binary patch literal 3221 zcmai03p|r;8-GR#m8d!P(yXI=+icD=D@;SE*QwFTY|mKDwzkKqlu}Mb67>#|r6Qsd zIw+w7l@1O$q~j+_a>}9l65q2C@$2h*_PcjqyYB1%zy8;K-Ov47%z9_)TtzKK008FN z+fiKL86_F=a`3ls=gt9mLI_=`)Umcgl&014ly5)m4v__0HFoU zl-dB`fmk?UZ$aPz8Q8A^4;I2gYAeHba1#NmR1g0Mk` zSf0Qahc`Dj#~BcC1Of(*zz9RQ5Iq>f6)qkl`N=~8g$x0k53zY%l!TY=!wY~c(P&Ac ziDyhF#AZ!q;tI#hf(yh+R&aQ%0q%D&CSyV#KS02dieoZxAP4jZxsVXX;eU&>f*=9g zX96opDD?zLv|=S`=}Zaw_gUdBo&uG&KmrP;^T$Bay&Nl%Bq@OC=>iIQ94S57+=L{p zYKn7mPsRYW-vNGgP6w4>_T zf-HIf2SStGxl$@7iU|tYzFZUr1#iRu7C4n(DsZ|K(%3N}S}}!FrTo8*lAd-xT_6PA zLinI1TGGn^0RxXgG`b6FL znO%N)?kHp;JzULBd$SF>pE{FAE}#Kxrfoub6uq1o3T zSm`fQufF2YZdt#rdqVHts1Un3?aoK0Y)*%Hd0o#ZtGZ5}a+0nH z(RXr}&VKD4^p_zW(f1y)O6JnM_Dz+96sH907DcNdoELTrC8tQ$O04Ys4%r=HuQG?~ zMw|JWjKnjW*QcJ>`k3~ir?TwDHG^DYRV^jKSg8`B^0nt?Z}Cm}_`L+ETCImy-I{|20dSlGEh-y(>Epuk}KvyCR~H7GRRSLikSpbJ&lV z$haqJ$8uL4ymf=Mdw75y8yb4V92@_9?!v#D8xDsleWH$3Yo31IQd33NY=2b>45S5U zmbqHRd;!WSrwq3jYb!^bA~m)gsy?i(F?w!x9PU>5L5t(aN>JVwdheW8(X_F|-{RRd z8P&DNeV~UPxwz2}x3jm7o_@K`nUR^6NK)7#|4#L;n;UX@#OT?a+AT`oaI4Cft}GSw zqK5nW%$9g1IyciCJPTjxUS#Km3tMD7*+t2IuIj19IFmAyZ&@14A{=QfwJWzn-fUr- z_*axQs2}OF&|IkUxQ4WAiTlcL1M#{day#L9JU`2&tn{cD zzovS1le$5olbViU0P^dp+CjtGk|6ryK4JFOD@xAL3FyqRCFeVrT-p{7nt3QvV^5mR z6u-BM{6^G8Bu3UIR2IDLrPt?qGN`g4w~f_M?81-F7pOljc*hHFI_~9Go91nSS} z7zc5lb*$n>Lh^kbi^zno)P0HtIr^4!(mX16t;W3Eqxbg*Z@(_DIgNE^-RG4T7M9hU z0`3FZ;fl-yTx!<{pE_&jKX2?Wee;2ka&;~RDVEAYk1{h^CQ z94qVGW%~DN@wXqT>qS5CyAFnj2~ayJMTNPWu|wPKb*l`%ulvE`sMdVEb4jmJXk)oA z+^&wGH0u087&CG=sy`BP4okTBuVuFuXg7*({>LLz_umd{9Pc*{JR5!QSd+ed?^Y+r z))~8+<*v>fGTzlaG-uU6_cTqt2(8wrg~AYQ@7iIrv`=7 wI40f83^Ap7@$_P5|ZY~ojFM@1wq9D3aB{BRn8oxX_`4P(_Cexv|MFR zX^w`Ora3AzXSqtvzMze#Uj3fmhu7sE&OPtmE=eu`0KjW*W^6~F z4H$!io&H^k3mT?ROeDJl`vEmw(v$QbSFuQQFKcVSZo16@U}q8nurNaC9{`gifNj$T z0Gyble%eQwAOL2%Uywe$nD%bk0(6^_fCI38k58n}jK_mM=uBI)5egrI0xH{K@fa)- zOH=^>%&<5=KOE2u>+kPP#Nq(}mLjHK`HPr;@);Dd{IcnM0Q*615BkIzXm*$c0Px*o z45qWHd{O`a8wKZpBqOb@V4j2kH57(`#;Q>Q0vRX(oC2ep0a!8$ND1)AlVB8mxy=X| z-DbGKa=^_HvY);j(%KeiL?B{;I%?`_>T(D!AP@*AV!U8>#-`ul^k4dNK4fwr3=9qp z4OI)(R3i|*!4N$?J+Qh4SVIFuj{uRv@MIJPgeS>wk^JB>#*#dVxIi+FfCn;oQD{Oi zSzk_$k?7~MrIU>F`jrV!`d${jKrmwk3{g`D{|$!m{3$OmnCQPL4&w>N`eOsIcruBO zgZvd|NG20;=$}|d!c9*wL#rAiEegXx|2->xi+_V|wtxYmpaQo*n|rxcBAihG?64h{ zMDX9FN9gImH>>*1`D;(M0LZ@q{_Xq|aFB?jmyBW*fPj2Y@NeJ0;xV2uPa+mYCJ_In zldg8N`S#eL-@QK;{Bejs&NCd$=qla+i<=R=>HVDs4*qLSINeLek^QlMFfeEh{mH+3 zf9YTxPMALm@2xLK0b#ISs9=Ax9KsR5NreGouq2!}9tZ-`x8eU5_&fimz(1wHw~h(S z5JUQ1%Kx`fo2NYxMI>Pz!UD1Sa*SRE6Fsp%Uk`dMKc632904O^-Ibyscnddo|{2@za$x_f91?ZNYEs0S_w#V}`x9AoC@mw$!WQsNb6u|< zxK8J`Sc;sNp6t4#Nt&g3-O@oGj_l-I|6Dp98-GAHr6i1I=P8u0=1 z@QlYbi7{zsxxJp*w*v4(6=GS{v_<5PCfunjBq^CHY8vXP&PU#bQ{vzpg6>o2;)tPJ*ZeD%-@x)_puK{ ze6TLh>s~fZKE~GYO#(E>EwgRRLAW5EY8CXz?68U^w!ubjMA2bd1aI*kZ0|tX-5fpp zBIEjC8ZBjdzww$xntdyR|5Sxfn0(_y(4%Ca``E4-7cx_Xe(7!Td7iFkT`HHA>hJ9I zyj3Yd9gpZe?rT|)-E|du+dgD>(Ifk2>W&Xj&__&KmPKvv2kkU^bDb+&^RNSRI%%S5JF{zEUbj~y`Y+`Qf#^1I1LQQ1!UJCkqUA`7zZsLIuxtk8E- z>pX3@ip^{F;VQK^C_&ycd@gcPBrZm51$BPB2wV_5x5V2Wpqo3^>iA`;VtQ^a)uTZ!av>%sk&0z#QSp?s%0hlF z(wxCuJv{K%lAIpRQ{~i(B7@a|tH^LEOMXQY!xrB_0|$bPUHYXOn?o}*C2CqJ;VZU% z8bjXz!_MOms6s>ot)F5)BOU^b}L86N-xGI zB~>R!Hr8A!mNh>FId*I;Bs3-B%8}L3x$KPxQG19xPTA_YQ~8fd#PD-pF7W`r9i&KG zwr8SWlT)(3ZHTH=RU{R;7QKJzr1i|8DzR_6wl*g%AqrT*KP-HC)s%d&W;#yRjp>SehMSOVqaGw+Jmj=>P-&G4*aM1dN(f~ z)YBc5eJ&;$A@P<{FZ#NzZV3iKCGTg0gzxK_Co;z-7}|sYx*~a0S=*vRbMA&T)mw5O zB~DBP7d|sQW0Ay#7%I*do7`I`Gi9%4dnEv(rTnqXQt__Om2zZ{G?Zk@p0;Bl-pyEV zd9B3pGPQ_U^Pupcs?*Fu6)v^Ytd2P{hv@bAQMrL-X7ST<>Ui4T;KHfFPt?x-b$OL@ z&TmK8pkL0C3O>(};cKUO)|c68zcHUQeD!59)sD6n4SiZ}Sy_HyplQK6Xk|xd>tqzB zzBBBj;*4Rqu=J8YnS~b-Ft_WQWEDW>bwL_#f+OR@u^O7frM32n`OV8C4BBr(6%Gk@5XNQ2M@IajB`p8RnyHXqF&l zTcgrT1$aTLu(v|Y0?#|{iy49&ZD)NR6sTRiH-wDR%I{6q;aY|$W_YLP-DOve{>oW2 zv2ar)P?m&|&b~H>ls5uz*tm$RWpxnlR8*J7NuC{fDxGaNySEy;rxC7mdpGs;q;(ds zVdu`nZ16!Qo`tT6(Iau99_qA#WM<4M-qwI>SLaC+yB7_2K_Gs8<*!PTPqLqoR+Sgq zV}*n2RgIbMZZlTMewQ=ctf;ZN=Z@@`i6*nbGm?}2%g&4A$7@|rAa_~&WwB|hJPIj{ zM7MCM*Bpp33yD?<%FUrM zb&yDf_J^!D?o;c`?7j^tb&1brd}?Y^yx%bN?)k_Xr(HA=;wH5JJw3RzSa;voF-I)+ zZHY}2xRT~71a4BmO8MPaxnK1JsdR#N*;iFu!taE#fDj9u-!=}em(C%gmX_-YxmYtd zE$aPPyliY9mHTDh3v|1H#dbbPWM>sowoI0K!E9&I#7OZcqrlcvxh2c9Oe{SuezoQ= z@2sF*T(WM2hexJOvrU>!_wv)uaQB%lq24fVxt`~u7LQ_|(~8Vz2ISdRURb=$bfpSp z%J80&rL{c?%pQFrdgY8C^N@98q4&jW@hsuu{KoSwnTYesCmXLsLhRfujicA|a?eJp z()tr*1e+DE7len3$LsAG7^WPtnEP_G;i&GyvfJnZ;6)frJ+-}KSM*&n5XA87TgQF;=s6OixfGap3BBagOhyZ3eC^*xu{ZgaBK zk0h=ZE)Qr+Qu=_q5-BgQ?x|+CH(xg|M*X6wVDFIiQWZSYsV^l%%D%fhp{p=QY~Hu} zXv|x^5=db>prs9H-E1%GFgsN5n)#3GqgAzX_cRlVP|vjF1u*5|YyL@+CW9DZ1mxltY$EOC)LJezJ=Z>#vJ>kntpa6W; z69bj^?;}Kw9QXInPdu)H*`w75&k1sQr69?Xw2%^$sYLy{_PPzhYAR4*k-%zjhIa*k zhWzu9+(%Q}Gnl|eCiadVlL=e|hMcfAsSD|_=tjatm&aSrHnioGgc&#;X_Tp|@kWs$ zT{NY6Zb6+?v04L(S#J-%jOG%q%T^1$@gbpKPbTt3InNtel-e5bnLHJzEwpW1nzQEX z(_XpDarg{o2qUn>rcT7%=cjC&n^b(KDm0wV6zd&(h(n6~OSj22Q6G^?83{hC;D9zI zE(7np6KQ)9M^luuFxPy?+#LbrF+X;@_47OngI>Ff5H+Ie?SZfga=C~o0E#!7zOV?W zqSi|A7gZH>UVS>|UQ&~MAzoF4L_X + + + + Youtube Auto Pause options + + + + +

+
+

YouTube Auto Pause

+
+
+
+

Settings

+ + + + + + + + + + +
+
+
+ +
+
+

Enabled sites

+
+

Other settings

+ +
+
+
+
+

Hotkeys

+
+
+

You can configure hotkeys in the Extensions menu

+
+
+
+

Source

+

+
+
+ +
+ + + + diff --git a/firefox/options.js b/firefox/options.js new file mode 100644 index 0000000..a8a334a --- /dev/null +++ b/firefox/options.js @@ -0,0 +1,118 @@ +const options = { + autopause: true, + autoresume: true, + scrollpause: false, + lockpause: true, + lockresume: true, + focuspause: false, + focusresume: false, + disabled: false, + cursorTracking: false, + manualPause: true, + debugMode: false, +}; + +const hosts = browser.runtime.getManifest().host_permissions; +for (const host of hosts) { + options[host] = true; +} + +// Saves options to browser.storage +function save_options() { + const storage = {}; + + for (const option in options) { + storage[option] = document.getElementById(option).checked; + } + + browser.storage.sync.set(storage, function () {}); +} + +// Restores options from browser.storage +function restore_options() { + browser.storage.sync.get(options, function (items) { + for (const opt in items) { + document.getElementById(opt).checked = items[opt]; + } + + for (const option in options) { + document.getElementById(option).disabled = items.disabled; + if (items.disabled) { + document.getElementById("disabled").disabled = false; + } + } + }); +} + +// Show shortcuts in the options window +browser.commands.getAll(function (commands) { + const hotkeysDiv = document.getElementById("hotkeys"); + for (let i = 0; i < commands.length; i++) { + if ( + commands[i].shortcut.length === 0 || + commands[i].description.length === 0 + ) { + continue; + } + const tag = document.createElement("p"); + const text = document.createTextNode( + commands[i].shortcut + " - " + commands[i].description + ); + tag.appendChild(text); + hotkeysDiv.appendChild(tag); + } +}); + +function formatHostName(hostname) { + return hostname.replace("https://", "").split("/")[0].replaceAll("*.", ""); +} + +const hostsDiv = document.getElementById("hosts"); +for (host of hosts) { + const label = document.createElement("label"); + const checkbox = document.createElement("input"); + checkbox.type = "checkbox"; + checkbox.id = host; + label.appendChild(checkbox); + const span = document.createElement("span"); + span.className = "label-text"; + span.textContent = formatHostName(host); + label.appendChild(span); + hostsDiv.appendChild(label); + checkbox.addEventListener("change", save_options); +} + +// Show version in the options window +const version = document.getElementById("version"); +version.textContent = "v" + browser.runtime.getManifest().version; + +// Restore options on load and when they change in the store +document.addEventListener("DOMContentLoaded", restore_options); +browser.storage.onChanged.addListener(function (_changes, _namespace) { + restore_options(); +}); + +// Listen to changes of options +for (const option in options) { + document.getElementById(option).addEventListener("change", save_options); +} + +const coll = document.getElementsByClassName("collapsible_button"); +let i; + +for (i = 0; i < coll.length; i++) { + coll[i].addEventListener("click", function () { + this.classList.toggle("active"); + const content = this.nextElementSibling; + if (content.style.maxHeight) { + content.style.maxHeight = null; + } else { + content.style.maxHeight = content.scrollHeight + "px"; + } + }); +} + +// Add event listener for the cursor tracking option +document + .getElementById("cursorTracking") + .addEventListener("change", save_options); diff --git a/firefox/yt.js b/firefox/yt.js new file mode 100644 index 0000000..fbcf2da --- /dev/null +++ b/firefox/yt.js @@ -0,0 +1,393 @@ +// Previous tab and window numbers +let previous_tab = -1; +let previous_window = browser.windows.WINDOW_ID_NONE; +let executedTabs = []; +let enabledTabs = []; + +// Computer state +let state = "active"; +// Default options +let options = { + autopause: true, + autoresume: true, + scrollpause: false, + lockpause: true, + lockresume: true, + focuspause: false, + focusresume: false, + disabled: false, + cursorTracking: false, + debugMode: false, +}; + +const hosts = browser.runtime.getManifest().host_permissions; +for (const host of hosts) { + options[host] = true; +} + +// Initialize settings from storage +setTimeout(() => { + refresh_settings(); +}, 100); + +function refresh_settings() { + browser.storage.sync.get(Object.keys(options), function (result) { + options = Object.assign(options, result); + if (options.disabled === true) { + options.autopause = false; + options.autoresume = false; + options.scrollpause = false; + options.lockpause = false; + options.lockresume = false; + options.focuspause = false; + options.focusresume = false; + options.cursorTracking = false; + options.debugMode = false; + for (var host of hosts) { + options[host] = false; + } + } + enabledTabs = []; + }); +} + +function debugLog(message) { + if (options.debugMode) { + console.log(`YouTube auto pause: ${message}`); + } +} + +function isEnabledForTab(tab) { + if (!tab || !tab.url) { + return false; + } + + if (enabledTabs.includes(tab.id)) { + return true; + } + + const optionKey = Object.keys(options).find((option) => { + if (!option.startsWith("http")) { + return false; + } + const reg = option + .replace(/[.+?^${}()|/[\]\\]/g, "\\$&") + .replace("*", ".*"); + return new RegExp(reg).test(tab.url); + }); + + if (optionKey) { + return !!options[optionKey]; + } + + return false; +} + +async function injectScript(tab) { + if (executedTabs.includes(tab.id) || !isEnabledForTab(tab)) { + return; + } + + debugLog(`Injecting script into tab ${tab.id} with url ${tab.url}`); + try { + await browser.scripting.executeScript({ + target: { tabId: tab.id }, + files: ["yt_auto_pause.js"], + }); + executedTabs.push(tab.id); + } catch (e) { + debugLog(e); + } +} + +// Functionality to send messages to tabs +function sendMessage(tab, message) { + if (!browser.runtime?.id || !isEnabledForTab(tab)) { + debugLog(`Not sending message`); + return; + } + + if (browser.runtime.lastError) { + console.error( + `YouTube Autopause error: ${browser.runtime.lastError.toString()}` + ); + return; + } + + debugLog(`Sending message ${JSON.stringify(message)} to tab ${tab.id}`); + + browser.tabs.sendMessage(tab.id, message, {}, function () { + void browser.runtime.lastError; + }); +} + +// Media conrol functions +function stop(tab) { + sendMessage(tab, { action: "stop" }); +} + +function resume(tab) { + sendMessage(tab, { action: "resume" }); +} + +function toggle(tab) { + sendMessage(tab, { action: "toggle" }); +} + +function mute(tab) { + sendMessage(tab, { action: "mute" }); +} + +function unmute(tab) { + sendMessage(tab, { action: "unmute" }); +} + +function toggle_mute(tab) { + sendMessage(tab, { action: "toggle_mute" }); +} + +// Listen options changes +browser.storage.onChanged.addListener(async function (changes) { + enabledTabs = []; + for (const key in changes) { + debugLog( + `Settings changed for key ${key} from ${changes[key].oldValue} to ${changes[key].newValue}` + ); + options[key] = changes[key].newValue; + } + + if ("disabled" in changes) { + const tabs = await browser.tabs.query({ active: true }); + if (!options.disabled) { + debugLog(`Extension enabled, resuming active tabs`); + } else { + debugLog(`Extension disabled, stopping active tabs`); + } + + for (let i = 0; i < tabs.length; i++) { + if (!options.disabled) { + resume(tabs[i]); + } else { + stop(tabs[i]); + } + } + } +}); + +// Tab change listener +browser.tabs.onActivated.addListener(function (info) { + if (previous_window !== info.windowId) { + return; + } + + browser.tabs.get(info.tabId, async function (tab) { + if (!isEnabledForTab(tab) || previous_tab === info.tabId) { + return; + } + + await injectScript(tab); + + if (options.autopause && previous_tab !== -1) { + debugLog(`Tab changed, stopping video from tab ${previous_tab}`); + browser.tabs.get(previous_tab, function (prev) { + stop(prev); + }); + } + + if (options.autoresume) { + debugLog(`Tab changed, resuming video from tab ${info.tabId}`); + resume(tab); + } + + previous_tab = info.tabId; + }); +}); + +// Tab update listener +browser.tabs.onUpdated.addListener(async function (tabId, changeInfo, tab) { + if (!isEnabledForTab(tab)) { + return; + } + + await injectScript(tab); + + if ( + "status" in changeInfo && + changeInfo.status === "complete" && + !tab.active + ) { + debugLog( + `Tab updated, stopping video in tab ${tabId} with status ${changeInfo.status}, active ${tab.active}` + ); + stop(tab); + } +}); + +browser.tabs.onRemoved.addListener(function (tabId) { + if (enabledTabs.includes(tabId)) { + debugLog(`Tab removed, removing tab ${tabId} from enabled tabs`); + enabledTabs = enabledTabs.filter((e) => e !== tabId); + } + if (executedTabs.includes(tabId)) { + debugLog(`Tab removed, removing tab ${tabId} from executed tabs`); + executedTabs = executedTabs.filter((e) => e !== tabId); + } +}); + +// Window focus listener +browser.windows.onFocusChanged.addListener(async function (window) { + if (window !== previous_window) { + if (options.focuspause && state !== "locked") { + const tabsStop = await browser.tabs.query({ windowId: previous_window }); + debugLog(`Window changed, stopping videos in window ${window}`); + for (let i = 0; i < tabsStop.length; i++) { + if (!isEnabledForTab(tabsStop[i])) { + continue; + } + stop(tabsStop[i]); + } + } + + if (options.focusresume && window !== browser.windows.WINDOW_ID_NONE) { + const tabsResume = await browser.tabs.query({ windowId: window }); + debugLog(`Window changed, resuming videos in window ${window}`); + for (let i = 0; i < tabsResume.length; i++) { + if (!isEnabledForTab(tabsResume[i])) { + continue; + } + if (!tabsResume[i].active && options.autopause) { + continue; + } + resume(tabsResume[i]); + } + } + + previous_window = window; + } +}); + +// Message listener for messages from tabs +browser.runtime.onMessage.addListener(async function ( + request, + sender, + sendResponse +) { + if (!isEnabledForTab(sender.tab) || browser.runtime.lastError) { + return true; + } + + if ("minimized" in request) { + if (request.minimized && options.autopause) { + debugLog(`Window minimized, stopping videos in tab ${sender.tab.id}`); + stop(sender.tab); + } else if (!request.minimized && options.autoresume) { + debugLog(`Window returned, resuming videos in tab ${sender.tab.id}`); + resume(sender.tab); + } + } + + if ("visible" in request && options.scrollpause) { + if (!request.visible) { + debugLog( + `Window is not visible, stopping videos in tab ${sender.tab.id}` + ); + stop(sender.tab); + } else { + debugLog(`Window is visible, resuming videos in tab ${sender.tab.id}`); + resume(sender.tab); + } + } + + await browser.storage.sync.get("cursorTracking", function (result) { + if (result.cursorTracking && "cursorNearEdge" in request) { + // Handle cursor near edge changes + if (request.cursorNearEdge && options.autopause) { + debugLog( + `Nearing window edge, stopping videos in tab ${sender.tab.id}` + ); + stop(sender.tab); + } else if (!request.cursorNearEdge && options.autoresume) { + debugLog(`Returned to window, resuming videos in tab ${sender.tab.id}`); + resume(sender.tab); + } + } + }); + + sendResponse({}); + return true; +}); + +// Listener for keyboard shortcuts +browser.commands.onCommand.addListener(async (command) => { + if (command === "toggle-extension") { + options.disabled = !options.disabled; + debugLog( + `Toggle extension command received, extension state ${options.disabled}` + ); + browser.storage.sync.set({ disabled: options.disabled }); + refresh_settings(); + } else if (command === "toggle-play") { + debugLog( + `Toggle play command received, toggling play for all tabs in current window` + ); + const tabs = await browser.tabs.query({ currentWindow: true }); + for (let i = 0; i < tabs.length; i++) { + if (!isEnabledForTab(tabs[i])) { + continue; + } + toggle(tabs[i]); + } + } else if (command === "toggle_mute") { + debugLog( + `Toggle mute command received, toggling mute for all tabs in current window` + ); + const tabs = await browser.tabs.query({ currentWindow: true }); + for (let i = 0; i < tabs.length; i++) { + if (!isEnabledForTab(tabs[i])) { + continue; + } + toggle_mute(tabs[i]); + } + } +}); + +// Listener for computer idle/locked/active +browser.idle.onStateChanged.addListener(async function (s) { + state = s; + const tabs = await browser.tabs.query({ active: true }); + + for (let i = 0; i < tabs.length; i++) { + if (!isEnabledForTab(tabs[i])) { + continue; + } + + if (state === "locked" && options.lockpause) { + debugLog(`Computer locked, stopping all videos`); + stop(tabs[i]); + } else if (state !== "locked" && options.lockresume) { + if (!tabs[i].active && options.autopause) { + continue; + } + debugLog(`Computer unlocked, resuming videos`); + resume(tabs[i]); + } + } +}); + +browser.windows.onCreated.addListener(async function (window) { + const tabs = await browser.tabs.query({ windowId: window.id }); + debugLog(`Window created, stopping all non active videos`); + for (let i = 0; i < tabs.length; i++) { + if (!isEnabledForTab(tabs[i])) { + continue; + } + + await injectScript(tabs[i]); + + if (tabs[i].active && options.autoresume) { + resume(tabs[i]); + } else if (options.autopause) { + stop(tabs[i]); + } + } +}); diff --git a/firefox/yt_auto_pause.js b/firefox/yt_auto_pause.js new file mode 100644 index 0000000..3d5e907 --- /dev/null +++ b/firefox/yt_auto_pause.js @@ -0,0 +1,245 @@ +if (window.ytAutoPauseInjected !== true) { + window.ytAutoPauseInjected = true; + let manuallyPaused = false; + let automaticallyPaused = false; + + let options = { + autopause: true, + autoresume: true, + scrollpause: false, + lockpause: true, + lockresume: true, + focuspause: false, + focusresume: false, + disabled: false, + cursorTracking: false, + manualPause: true, + debugMode: false, + }; + + function debugLog(message) { + if (options.debugMode) { + console.log(`YouTube auto pause: ${message}`); + } + } + + // Initialize settings from storage + refresh_settings(); + + function refresh_settings() { + browser.storage.sync.get(Object.keys(options), function (result) { + options = Object.assign(options, result); + if (options.disabled === true) { + options.autopause = false; + options.autoresume = false; + options.scrollpause = false; + options.lockpause = false; + options.lockresume = false; + options.focuspause = false; + options.focusresume = false; + options.cursorTracking = false; + options.debugMode = false; + for (var host of hosts) { + options[host] = false; + } + } + }); + } + + browser.storage.onChanged.addListener(async function (changes, namespace) { + for (const key in changes) { + debugLog( + `Settings changed for key ${key} from ${changes[key].oldValue} to ${changes[key].newValue}` + ); + options[key] = changes[key].newValue; + } + + if (!options.manualPause) { + manuallyPaused = false; + automaticallyPaused = true; + } + + if ("disabled" in changes) { + refresh_settings(); + } + }); + + // Function to check if the cursor is near the edge of the window + function isCursorNearEdge(event) { + const threshold = 50; // pixels from the edge + return ( + event.clientX < threshold || + event.clientX > window.innerWidth - threshold || + event.clientY < threshold || + event.clientY > window.innerHeight - threshold + ); + } + + let cursorNearEdgeTimeout; + + // Listen for mousemove events + window.addEventListener("mousemove", async function (event) { + if (!options.cursorTracking) { + return; + } + if (isCursorNearEdge(event)) { + // If the cursor is near the edge, set a timeout + if (!cursorNearEdgeTimeout) { + cursorNearEdgeTimeout = setTimeout(function () { + debugLog(`Cursor near window edge, sending message`); + sendMessage({ cursorNearEdge: true }); + cursorNearEdgeTimeout = null; + }, 200); // Wait for 1 second to infer user intention + } + } else { + // If the cursor moves away from the edge, clear the timeout + clearTimeout(cursorNearEdgeTimeout); + cursorNearEdgeTimeout = null; + debugLog(`Cursor not near window edge, sending message`); + sendMessage({ cursorNearEdge: false }); + } + }); + + // Existing code... + + // Send message to service worker + function sendMessage(message) { + if (!browser.runtime?.id) { + return; + } + + if (browser.runtime.lastError) { + console.error( + `Youtube Autopause error: ${browser.runtime.lastError.toString()}` + ); + return; + } + + debugLog(`Sending message ${JSON.stringify(message)}`); + + browser.runtime.sendMessage(message, function () { + void browser.runtime.lastError; + }); + } + + // Listen to visibilitychange event of the page + document.addEventListener( + "visibilitychange", + function () { + if (document.hidden !== undefined) { + debugLog(`Document hidden, sending minimized ${document.hidden}`); + sendMessage({ minimized: document.hidden }); + } + }, + false + ); + + // Listen media commands from the service worker + browser.runtime.onMessage.addListener(async function ( + request, + sender, + sendResponse + ) { + if (!("action" in request)) { + return false; + } + debugLog(`Received message: ${JSON.stringify(request)}`); + + const videoElements = document.getElementsByTagName("video"); + const iframeElements = document.getElementsByTagName("iframe"); + + for (let i = 0; i < iframeElements.length; i++) { + const iframe = iframeElements[i]; + try { + if (request.action === "stop") { + iframe.contentWindow.postMessage( + JSON.stringify({ event: "command", func: "pauseVideo" }), + "*" + ); + } else if (request.action === "resume") { + iframe.contentWindow.postMessage( + JSON.stringify({ event: "command", func: "playVideo" }), + "*" + ); + } + } catch (e) { + debugLog(e); + } + } + + for (let i = 0; i < videoElements.length; i++) { + try { + if (request.action === "stop" && !manuallyPaused) { + automaticallyPaused = true; + videoElements[i].pause(); + } else if ( + request.action === "resume" && + videoElements[i].paused && + !manuallyPaused + ) { + automaticallyPaused = false; + if (!videoElements[i].ended) { + await videoElements[i].play(); + } + } else if (request.action === "toggle_mute") { + videoElements[i].muted = !videoElements[i].muted; + } else if (request.action === "mute") { + videoElements[i].muted = true; + } else if (request.action === "unmute") { + videoElements[i].muted = false; + } else if (request.action === "toggle") { + if (videoElements[i].paused && !manuallyPaused) { + if (!videoElements[i].ended) { + await videoElements[i].play(); + } + automaticallyPaused = false; + } else if (!manuallyPaused) { + videoElements[i].pause(); + automaticallyPaused = true; + } + } + } catch (e) { + debugLog(e); + } + } + sendResponse({}); + return true; + }); + + // Intersection observer for the video elements in page + // can be used to determine when video goes out of viewport + const intersection_observer = new IntersectionObserver( + function (entries) { + if (!options.scrollpause) { + return; + } + if (entries[0].isIntersecting === true) { + debugLog(`Video not anymore in viewport`); + sendMessage({ visible: true }); + } else { + debugLog(`Video in viewport`); + sendMessage({ visible: false }); + } + }, + { threshold: [0] } + ); + + // Start observing video elements + let videoElements = document.getElementsByTagName("video"); + for (let i = 0; i < videoElements.length; i++) { + intersection_observer.observe(videoElements[i]); + videoElements[i].addEventListener("pause", async (_e) => { + if (!automaticallyPaused && options.manualPause) { + debugLog(`Manually paused video`); + manuallyPaused = true; + automaticallyPaused = false; + } + }); + videoElements[i].addEventListener("play", (_e) => { + if (options.manualPause) { + debugLog(`Manually resumed video`); + manuallyPaused = false; + } + }); + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..855152a --- /dev/null +++ b/package.json @@ -0,0 +1,38 @@ +{ + "name": "@drodil/youtube_auto_pause", + "version": "1.7.0", + "homepage": "https://github.com/drodil/youtube_auto_pause", + "bugs": { + "url": "https://github.com/drodil/youtube_auto_pause/issues", + "email": "heiccih@gmail.com" + }, + "private": true, + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/drodil/youtube_auto_pause.git" + }, + "funding": [ + { + "type": "patreon", + "url": "https://www.patreon.com/drodil" + }, + { + "type": "github", + "url": "https://github.com/sponsors/drodil" + } + ], + "scripts": { + "build": "npm run build:chrome && npm run build:firefox", + "lint": "web-ext lint --source-dir ./firefox/", + "start:chrome": "web-ext run --source-dir ./chrome/ --target chromium", + "debug:chrome": "web-ext run --devtools --source-dir ./chrome/ --target chromium", + "build:chrome": "web-ext build --overwrite-dest --source-dir ./chrome/ --filename chrome.zip", + "start:firefox": "web-ext run --source-dir ./firefox/", + "debug:firefox": "web-ext run --devtools --source-dir ./firefox/", + "build:firefox": "web-ext build --overwrite-dest --source-dir ./firefox/ --filename firefox.zip" + }, + "devDependencies": { + "web-ext": "^8.2.0" + } +}