├── .github └── workflows │ └── main.yml ├── LICENSE ├── README.md ├── attach.js ├── audio.png ├── background.js ├── content.js ├── default.css ├── icon.png ├── manifest.json ├── options.html ├── options.js ├── popup.css ├── popup.html └── popup.js /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: CI 4 | 5 | # Controls when the workflow will run 6 | on: 7 | # Triggers the workflow on push but only when manifest.json (is changed) 8 | push: 9 | branches: [ main ] 10 | paths: 11 | 'manifest.json' 12 | 13 | # Allows to run this workflow manually 14 | workflow_dispatch: 15 | 16 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 17 | jobs: 18 | # This workflow contains a single job called "build" 19 | build: 20 | # The type of runner that the job will run on 21 | runs-on: ubuntu-latest 22 | 23 | # Steps represent a sequence of tasks that will be executed as part of the job 24 | steps: 25 | # Runs a set of commands using the runners shell 26 | - name: run build script 27 | env: 28 | ISSUER: ${{secrets.ISSUER}} 29 | SECRET: ${{secrets.SECRET}} 30 | run: curl 'https://raw.githubusercontent.com/igorlogius/meta-addon-builder/main/README' | sh 31 | 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2022 igorlogius 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | 7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![](https://raw.githubusercontent.com/igorlogius/igorlogius/main/geFxAddon.png)](https://addons.mozilla.org/firefox/addon/tabs-media-controller/) 2 | 3 | [Report a bug, make a suggestion or ask a question](https://github.com/igorlogius/igorlogius/issues/new/choose) 4 | 5 | [demo-2025-04-23_12.20.19.webm](https://github.com/user-attachments/assets/2ee71ed6-e15a-49c5-b404-7a803f5b30e1) 6 | -------------------------------------------------------------------------------- /attach.js: -------------------------------------------------------------------------------- 1 | // attach unattached audio objects to the DOM, when playback starts to make them detectable 2 | (() => { 3 | const originalProto = Audio.prototype; 4 | ["play"].forEach((method) => { 5 | const originalMethod = Audio.prototype[method]; 6 | Audio.prototype[method] = function () { 7 | Audio.prototype = originalProto; 8 | if (!document.contains(this)) { 9 | document.body.appendChild(this); 10 | } 11 | return originalMethod.apply(this, arguments); 12 | }; 13 | }); 14 | })(); 15 | -------------------------------------------------------------------------------- /audio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/igorlogius/tabs-media-controller/b7a7f97c289aa1cd2dc97425227e55c1aad90467/audio.png -------------------------------------------------------------------------------- /background.js: -------------------------------------------------------------------------------- 1 | /* global browser */ 2 | 3 | const manifest = browser.runtime.getManifest(); 4 | const extname = manifest.name; 5 | 6 | async function onCommand(cmd) { 7 | let qryObj = { hidden: false, currentWindow: true }; 8 | let tabs = [], 9 | atab; 10 | 11 | for (const t of await browser.tabs.query(qryObj)) { 12 | if (t.active === true) { 13 | atab = { windowId: t.windowId, index: t.index }; 14 | } 15 | if (t.audible === true) { 16 | tabs.push(t.index); 17 | } 18 | } 19 | 20 | if (tabs.length < 1 || (tabs.length === 1 && atab.index === tabs[0])) { 21 | return; 22 | } 23 | switch (cmd) { 24 | case "gotoNextRight": 25 | tabs.sort((a, b) => { 26 | return a - b; 27 | }); 28 | if (atab.index === tabs[tabs.length - 1]) { 29 | browser.tabs.highlight({ 30 | windowId: atab.windowId, 31 | tabs: [tabs[0]], 32 | }); // overflow 33 | return; 34 | } 35 | for (const tidx of tabs) { 36 | if (tidx > atab.index) { 37 | browser.tabs.highlight({ windowId: atab.windowId, tabs: [tidx] }); 38 | return; 39 | } 40 | } 41 | break; 42 | case "gotoNextLeft": 43 | tabs.sort((a, b) => { 44 | return b - a; 45 | }); 46 | if (atab.index === tabs[tabs.length - 1]) { 47 | browser.tabs.highlight({ 48 | windowId: atab.windowId, 49 | tabs: [tabs[0]], 50 | }); // overflow 51 | } 52 | for (const tidx of tabs) { 53 | if (tidx < atab.index) { 54 | browser.tabs.highlight({ windowId: atab.windowId, tabs: [tidx] }); 55 | return; 56 | } 57 | } 58 | break; 59 | } 60 | } 61 | 62 | browser.commands.onCommand.addListener(onCommand); 63 | 64 | browser.runtime.onInstalled.addListener(async (details) => { 65 | if (details.reason === "install") { 66 | let tmp = await fetch(browser.runtime.getURL("default.css")); 67 | tmp = await tmp.text(); 68 | browser.storage.local.set({ styles: tmp }); 69 | } 70 | }); 71 | -------------------------------------------------------------------------------- /content.js: -------------------------------------------------------------------------------- 1 | /*global browser */ 2 | 3 | function getMediaElementBy(id) { 4 | return document.querySelector('[tmcuuid="' + id + '"]'); 5 | } 6 | 7 | function getThumbnail(video) { 8 | let canvas = document.createElement("canvas"); 9 | let ctx = canvas.getContext("2d"); 10 | ctx.drawImage(video, 0, 0, 300, 200); 11 | return canvas.toDataURL("image/jpeg", 0.3); 12 | } 13 | 14 | function handleQuery(id) { 15 | const el = getMediaElementBy(id); 16 | 17 | return { 18 | poster: 19 | el.tagName.toLowerCase() === "video" ? getThumbnail(el) : "audio.png", 20 | duration: el.duration, 21 | currentTime: el.currentTime, 22 | playing: !el.paused, 23 | volume: el.volume, 24 | muted: el.muted, 25 | playbackRate: el.playbackRate, 26 | id, 27 | }; 28 | } 29 | 30 | // get all media elements and their states 31 | function handleQueryAll() { 32 | const ret = []; 33 | const els = document.querySelectorAll("video,audio"); 34 | for (const el of els) { 35 | let tmcuuid = el.getAttribute("tmcuuid"); 36 | if (tmcuuid === null) { 37 | tmcuuid = crypto.randomUUID(); 38 | el.setAttribute("tmcuuid", tmcuuid); 39 | } 40 | 41 | if ( 42 | !isNaN(el.duration) // https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/duration 43 | ) { 44 | ret.push({ 45 | poster: el.tagName.toLowerCase() === "video" ? getThumbnail(el) : "", 46 | type: el.tagName.toLowerCase(), 47 | duration: el.duration, 48 | currentTime: el.currentTime, 49 | playing: !el.paused, 50 | volume: el.volume, 51 | muted: el.muted, 52 | playbackRate: el.playbackRate, 53 | id: tmcuuid, 54 | }); 55 | } 56 | } 57 | 58 | return ret; 59 | } 60 | 61 | function handlePreview(id) { 62 | let el = getMediaElementBy(id); 63 | if (el) { 64 | return getThumbnail(el); 65 | } 66 | return ""; 67 | } 68 | 69 | function handlePause(ids) { 70 | for (const id of ids) { 71 | let el = getMediaElementBy(id); 72 | if (el) { 73 | el.pause(); 74 | } 75 | } 76 | return "play"; 77 | } 78 | 79 | function handlePauseAll() { 80 | for (const el of document.querySelectorAll("video,audio")) { 81 | el.pause(); 82 | } 83 | } 84 | 85 | function handleMuteAll() { 86 | for (const el of document.querySelectorAll("video,audio")) { 87 | el.muted = true; 88 | } 89 | } 90 | 91 | function handlePlay(ids) { 92 | for (const id of ids) { 93 | let el = getMediaElementBy(id); 94 | if (el) { 95 | el.play(); 96 | } 97 | } 98 | } 99 | 100 | function handleMute(ids) { 101 | for (const id of ids) { 102 | let el = getMediaElementBy(id); 103 | if (el) { 104 | el.muted = true; 105 | } 106 | } 107 | } 108 | 109 | function handleUnMute(ids) { 110 | for (const id of ids) { 111 | let el = getMediaElementBy(id); 112 | if (el) { 113 | el.muted = false; 114 | } 115 | } 116 | } 117 | 118 | function handlePIP(ids) { 119 | for (const id of ids) { 120 | let el = getMediaElementBy(id); 121 | if (el) { 122 | // not supported ref. https://developer.mozilla.org/en-US/docs/Web/API/HTMLVideoElement/requestPictureInPicture 123 | // and since bug https://bugzilla.mozilla.org/show_bug.cgi?id=1463402 124 | // is closed with WONTFIX, this feature wont be available any time soon it seems 125 | el.requestPictureInPicture(); 126 | } 127 | } 128 | } 129 | 130 | async function handleFullscreen(id) { 131 | let el = getMediaElementBy(id); 132 | if (el) { 133 | try { 134 | el.requestFullscreen(); // not doable since action is not recognized as a short running user action 135 | } catch (e) { 136 | console.error(e); 137 | } 138 | } 139 | } 140 | 141 | async function handlePlaybackRate(id, playbackRate) { 142 | let el = getMediaElementBy(id); 143 | if (el) { 144 | try { 145 | el.playbackRate = playbackRate; 146 | } catch (e) { 147 | console.error(e); 148 | } 149 | } 150 | } 151 | 152 | async function handleVolume(id, volume) { 153 | let el = getMediaElementBy(id); 154 | if (el) { 155 | try { 156 | el.volume = volume; 157 | } catch (e) { 158 | console.error(e); 159 | } 160 | } 161 | } 162 | 163 | async function handleCurrentTime(id, currentTime) { 164 | let el = getMediaElementBy(id); 165 | if (el) { 166 | try { 167 | el.currentTime = currentTime; 168 | } catch (e) { 169 | console.error(e); 170 | } 171 | } 172 | } 173 | 174 | async function handleFocus(id) { 175 | let el = getMediaElementBy(id); 176 | if (el) { 177 | try { 178 | el.scrollIntoView(); 179 | } catch (e) { 180 | console.error(e); 181 | } 182 | } 183 | } 184 | browser.runtime.onMessage.addListener((request) => { 185 | //console.debug("onMessage", JSON.stringify(request, null, 4)); 186 | switch (request.cmd) { 187 | case "query": 188 | return Promise.resolve(handleQuery(request.id)); 189 | case "queryAll": 190 | return Promise.resolve(handleQueryAll()); 191 | case "play": 192 | return Promise.resolve(handlePlay(request.ids)); 193 | case "pause": 194 | return Promise.resolve(handlePause(request.ids)); 195 | case "pauseAll": 196 | return Promise.resolve(handlePauseAll()); 197 | case "mute": 198 | return Promise.resolve(handleMute(request.ids)); 199 | case "muteAll": 200 | return Promise.resolve(handleMuteAll()); 201 | case "unmute": 202 | return Promise.resolve(handleUnMute(request.ids)); 203 | case "pip": 204 | return Promise.resolve(handlePIP(request.ids)); 205 | case "fullscreen": 206 | return Promise.resolve(handleFullscreen(request.ids)); 207 | case "volume": 208 | return Promise.resolve(handleVolume(request.id, request.volume)); 209 | case "playbackRate": 210 | return Promise.resolve( 211 | handlePlaybackRate(request.id, request.playbackRate), 212 | ); 213 | case "currentTime": 214 | return Promise.resolve( 215 | handleCurrentTime(request.id, request.currentTime), 216 | ); 217 | case "focus": 218 | return Promise.resolve(handleFocus(request.id)); 219 | case "preview": 220 | return Promise.resolve(handlePreview(request.id)); 221 | default: 222 | console.error("unknown request", request); 223 | break; 224 | } 225 | }); 226 | 227 | setTimeout(() => { 228 | //console.debug("injecting media.js into document head "); 229 | var s = document.createElement("script"); 230 | s.src = browser.runtime.getURL("attach.js"); 231 | s.onload = () => s.remove(); 232 | document.head.appendChild(s); 233 | }, 3000); 234 | -------------------------------------------------------------------------------- /default.css: -------------------------------------------------------------------------------- 1 | :root { 2 | /* dark mode */ 3 | /* 4 | --main-bg-color: gray; 5 | --main-btn-bgcolor: black; 6 | --main-btn-fontcolor: white; 7 | */ 8 | 9 | /* light mode */ 10 | /**/ 11 | --main-bg-color: lightgray; 12 | --main-btn-bgcolor: silver; 13 | --main-btn-fontcolor: black; 14 | /**/ 15 | } 16 | 17 | button { 18 | background-color: var(--main-btn-bgcolor); 19 | color: var(--main-btn-fontcolor); 20 | } 21 | 22 | button:hover { 23 | border-color: black; 24 | } 25 | 26 | body { 27 | max-width: 595px; 28 | padding: 10px; 29 | margin: 0; 30 | background-color: var(--main-bg-color); 31 | } 32 | 33 | #tabs { 34 | padding: 0; 35 | margin: 0; 36 | overflow-y: scroll; 37 | } 38 | .tabDiv { 39 | padding: 10px; 40 | margin: 0; 41 | } 42 | .tabFavImg { 43 | display: block; 44 | height: 1.5em; 45 | float: left; 46 | padding: 0; 47 | margin: 0; 48 | } 49 | .tabFocusBtn { 50 | display: inline-block; 51 | width: 100%; 52 | padding: 0; 53 | margin: 0; 54 | } 55 | .tabMuteBtn { 56 | width: 25%; 57 | } 58 | .tabPauseBtn { 59 | width: 25%; 60 | } 61 | .sitePauseBtn { 62 | width: 25%; 63 | } 64 | .siteMuteBtn { 65 | width: 25%; 66 | } 67 | .elementDiv { 68 | width: 100%; 69 | height: 175px; 70 | } 71 | .previewImg { 72 | height: 170px; 73 | float: left; 74 | border: 5px groove var(--main-bg-color); 75 | margin-left: 5px; 76 | padding-bottom: 3px; 77 | padding-right: 3px; 78 | width: 297px; 79 | } 80 | .elementControlsDiv { 81 | position: relative; 82 | border: 5px groove var(--main-bg-color); 83 | height: 162px; 84 | width: 250px; 85 | float: left; 86 | padding-top: 10px; 87 | } 88 | .elementFocusBtn { 89 | width: 70%; 90 | margin-left: 15%; 91 | } 92 | .elementMuteBtn { 93 | width: 70%; 94 | margin-left: 15%; 95 | } 96 | 97 | .elementPlaybackRateBtn { 98 | width: 70%; 99 | margin-left: 15%; 100 | } 101 | .elementVolumeBtn { 102 | width: 70%; 103 | margin-left: 15%; 104 | } 105 | .elementPlayPauseBtn { 106 | width: 70%; 107 | margin-left: 15%; 108 | } 109 | .elementTimeBtn { 110 | width: 70%; 111 | margin-left: 15%; 112 | } 113 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/igorlogius/tabs-media-controller/b7a7f97c289aa1cd2dc97425227e55c1aad90467/icon.png -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "igorlogius", 3 | "homepage_url": "https://github.com/igorlogius/tabs-media-controller", 4 | "description": "Control audio and video elements from all tabs via the toolbar button popup menu", 5 | "background": { 6 | "scripts": ["background.js"] 7 | }, 8 | "browser_action": { 9 | "default_area": "navbar", 10 | "default_popup": "popup.html" 11 | }, 12 | "commands": { 13 | "_execute_browser_action": { 14 | "suggested_key": { 15 | "default": "F1" 16 | } 17 | }, 18 | "gotoNextRight": { 19 | "description": "switch to next audible tab right" 20 | }, 21 | "gotoNextLeft": { 22 | "description": "switch to next audible tab left" 23 | } 24 | }, 25 | "content_scripts": [ 26 | { 27 | "js": ["content.js"], 28 | "matches": [""] 29 | } 30 | ], 31 | "icons": { 32 | "256": "icon.png" 33 | }, 34 | "manifest_version": 2, 35 | "name": "Tabs Media Controller", 36 | "options_ui": { 37 | "page": "options.html" 38 | }, 39 | "permissions": ["storage", "tabs"], 40 | "version": "1.5.24", 41 | "web_accessible_resources": ["attach.js"] 42 | } 43 | -------------------------------------------------------------------------------- /options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 20 | 21 |

Edit popup stylesheet infomation here

22 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /options.js: -------------------------------------------------------------------------------- 1 | /* global browser */ 2 | 3 | function onChange(evt) { 4 | let id = evt.target.id; 5 | let el = document.getElementById(id); 6 | 7 | let value = el.type === "checkbox" ? el.checked : el.value; 8 | let obj = {}; 9 | 10 | if (value === "") { 11 | return; 12 | } 13 | if (el.type === "number") { 14 | try { 15 | value = parseInt(value); 16 | if (isNaN(value)) { 17 | value = el.min; 18 | } 19 | if (value < el.min) { 20 | value = el.min; 21 | } 22 | } catch (e) { 23 | value = el.min; 24 | } 25 | } 26 | 27 | obj[id] = value; 28 | 29 | browser.storage.local.set(obj).catch(console.error); 30 | } 31 | 32 | ["styles"].map((id) => { 33 | browser.storage.local 34 | .get(id) 35 | .then((obj) => { 36 | let el = document.getElementById(id); 37 | let val = obj[id]; 38 | 39 | if (typeof val !== "undefined") { 40 | if (el.type === "checkbox") { 41 | el.checked = val; 42 | } else { 43 | el.value = val; 44 | } 45 | } 46 | }) 47 | .catch(console.error); 48 | 49 | let el = document.getElementById(id); 50 | el.addEventListener("input", onChange); 51 | }); 52 | 53 | document.getElementById("reset").addEventListener("click", async () => { 54 | if ( 55 | !confirm( 56 | "Are you sure?\nThe current stylesheet data will be lost when you click on ok.", 57 | ) 58 | ) { 59 | return; 60 | } 61 | 62 | let tmp = await fetch(browser.runtime.getURL("default.css")); 63 | tmp = await tmp.text(); 64 | let el = document.getElementById("styles"); 65 | el.value = tmp; 66 | browser.storage.local.set({ styles: tmp }); 67 | }); 68 | -------------------------------------------------------------------------------- /popup.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/igorlogius/tabs-media-controller/b7a7f97c289aa1cd2dc97425227e55c1aad90467/popup.css -------------------------------------------------------------------------------- /popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
no media to control found
7 | 8 | 9 | -------------------------------------------------------------------------------- /popup.js: -------------------------------------------------------------------------------- 1 | /* global browser */ 2 | 3 | async function getFromStorage(type, id, fallback) { 4 | let tmp = await browser.storage.local.get(id); 5 | return typeof tmp[id] === type ? tmp[id] : fallback; 6 | } 7 | 8 | const tablist = document.getElementById("tabs"); 9 | 10 | function addDataListToRange(numberArray, rangeEl, attachEl) { 11 | const datalistid = "rangeSteps" + Date.now(); 12 | 13 | rangeEl.setAttribute("list", datalistid); 14 | let datalistEl = document.createElement("datalist"); 15 | datalistEl.setAttribute("id", datalistid); 16 | 17 | numberArray.forEach((val) => { 18 | let opt = document.createElement("option"); 19 | opt.innerText = val; 20 | datalistEl.appendChild(opt); 21 | }); 22 | 23 | attachEl.appendChild(datalistEl); 24 | } 25 | 26 | async function queryTabs() { 27 | let first = true; 28 | 29 | const tabs = await browser.tabs.query({ 30 | url: [""], 31 | discarded: false, 32 | status: "complete", 33 | }); 34 | 35 | // lets move the audible tabs to the front/top 36 | tabs.sort((a, b) => { 37 | if (a.audible && !b.audible) { 38 | return -1; 39 | } 40 | if (b.audible && !a.audible) { 41 | return 1; 42 | } 43 | return 0; 44 | }); 45 | 46 | for (const tab of tabs) { 47 | try { 48 | const res = await browser.tabs.sendMessage(tab.id, { cmd: "queryAll" }); 49 | //console.debug(JSON.stringify(res, null, 4)); 50 | 51 | res.sort((a, b) => { 52 | if (a.playing && !b.playing) { 53 | return -1; 54 | } 55 | if (b.playing && !a.playing) { 56 | return 1; 57 | } 58 | if (a.playing && b.playing) { 59 | if (!a.muted && b.muted) { 60 | return -1; 61 | } 62 | if (a.muted && !b.muted) { 63 | return 1; 64 | } 65 | // both are muted or unmuted => equal 66 | } 67 | return 0; 68 | }); 69 | 70 | if (res.length > 0) { 71 | if (first) { 72 | tablist.textContent = ""; 73 | first = false; 74 | } 75 | 76 | const url = new URL(tab.url); 77 | const favIconUrl = tab.favIconUrl || ""; 78 | 79 | let tabdiv = document.createElement("div"); 80 | tabdiv.classList.add("tabDiv"); 81 | /*let seperator = document.createElement("hr"); 82 | tabdiv.appendChild(seperator);*/ 83 | 84 | tablist.appendChild(tabdiv); 85 | 86 | let tabFavImg = document.createElement("img"); 87 | tabFavImg.classList.add("tabFavImg"); 88 | tabFavImg.src = favIconUrl; 89 | let tablink = document.createElement("button"); 90 | 91 | tablink.textContent = 92 | "#" + 93 | tab.index + 94 | " " + 95 | url.hostname.replace(/^www\./, "") + 96 | " - " + 97 | tab.title; 98 | tablink.classList.add("tabFocusBtn"); 99 | tabdiv.appendChild(tablink); 100 | tablink.setAttribute("title", "focus tab"); 101 | tablink.onclick = () => { 102 | browser.tabs.highlight({ windowId: tab.windowId, tabs: [tab.index] }); 103 | }; 104 | tablink.appendChild(tabFavImg); 105 | 106 | let mutetabbtn = document.createElement("button"); 107 | mutetabbtn.textContent = "mute(tab)"; 108 | mutetabbtn.classList.add("tabMuteBtn"); 109 | mutetabbtn.setAttribute("title", "mute tab"); 110 | tabdiv.appendChild(mutetabbtn); 111 | mutetabbtn.onclick = async () => { 112 | browser.tabs.sendMessage(tab.id, { 113 | cmd: "muteAll", 114 | }); 115 | }; 116 | 117 | let pausetabbtn = document.createElement("button"); 118 | pausetabbtn.textContent = "pause(tab)"; 119 | pausetabbtn.classList.add("tabPauseBtn"); 120 | pausetabbtn.setAttribute("title", "pause tab"); 121 | tabdiv.appendChild(pausetabbtn); 122 | pausetabbtn.onclick = () => { 123 | browser.tabs.sendMessage(tab.id, { cmd: "pauseAll" }); 124 | }; 125 | 126 | let pauseOriginbtn = document.createElement("button"); 127 | pauseOriginbtn.textContent = "pause(site)"; 128 | pauseOriginbtn.classList.add("sitePauseBtn"); 129 | pauseOriginbtn.setAttribute("title", "pause site"); 130 | tabdiv.appendChild(pauseOriginbtn); 131 | pauseOriginbtn.onclick = () => { 132 | for (const tt of tabs) { 133 | if (tt.url.startsWith(url.origin)) { 134 | browser.tabs.sendMessage(tt.id, { cmd: "pauseAll" }); 135 | } 136 | } 137 | }; 138 | 139 | let muteOriginbtn = document.createElement("button"); 140 | muteOriginbtn.textContent = "mute(site)"; 141 | muteOriginbtn.classList.add("siteMuteBtn"); 142 | muteOriginbtn.setAttribute("title", "mute site"); 143 | tabdiv.appendChild(muteOriginbtn); 144 | muteOriginbtn.onclick = async () => { 145 | for (const tt of tabs) { 146 | if (tt.url.startsWith(url.origin)) { 147 | browser.tabs.sendMessage(tt.id, { 148 | cmd: "muteAll", 149 | }); 150 | } 151 | } 152 | }; 153 | 154 | for (const e of res) { 155 | // media element 156 | let elementrow = document.createElement("div"); 157 | elementrow.setAttribute("eid", e.id); 158 | elementrow.setAttribute("tid", tab.id); 159 | elementrow.classList.add("elementDiv"); 160 | tabdiv.appendChild(elementrow); 161 | 162 | // poster 163 | let previewImg = document.createElement("img"); 164 | previewImg.src = e.poster || "audio.png"; 165 | previewImg.classList.add("previewImg"); 166 | elementrow.appendChild(previewImg); 167 | 168 | let controls = document.createElement("div"); 169 | controls.classList.add("elementControlsDiv"); 170 | elementrow.appendChild(controls); 171 | 172 | // focus 173 | let focusbtn = document.createElement("button"); 174 | focusbtn.textContent = "focus"; 175 | focusbtn.classList.add("elementFocusBtn"); 176 | focusbtn.setAttribute("title", "focus element"); 177 | controls.appendChild(focusbtn); 178 | focusbtn.onclick = async (evt) => { 179 | await browser.windows.update(tab.windowId, { focused: true }); 180 | await browser.tabs.highlight({ 181 | windowId: tab.windowId, 182 | tabs: [tab.index], 183 | }); 184 | await browser.tabs.sendMessage(tab.id, { 185 | cmd: evt.target.textContent, 186 | id: e.id, 187 | }); 188 | }; 189 | 190 | // playbackRate 191 | let playbackRatebtn = document.createElement("input"); 192 | playbackRatebtn.setAttribute("type", "range"); 193 | playbackRatebtn.setAttribute("min", "25"); 194 | playbackRatebtn.setAttribute("max", "175"); 195 | playbackRatebtn.setAttribute("step", "1"); 196 | playbackRatebtn.setAttribute("value", e.playbackRate * 100); 197 | playbackRatebtn.classList.add("elementPlaybackRateBtn"); 198 | playbackRatebtn.setAttribute( 199 | "title", 200 | "Playback Rate: " + e.playbackRate, 201 | ); 202 | 203 | addDataListToRange( 204 | [25, 50, 75, 100, 125, 150, 175], 205 | playbackRatebtn, 206 | controls, 207 | ); 208 | controls.appendChild(playbackRatebtn); 209 | playbackRatebtn.addEventListener("input", (evt) => { 210 | browser.tabs.sendMessage(tab.id, { 211 | cmd: "playbackRate", 212 | id: e.id, 213 | playbackRate: evt.target.value / 100, 214 | }); 215 | 216 | evt.target.setAttribute( 217 | "title", 218 | "Playback Rate: " + evt.target.value / 100, 219 | ); 220 | }); 221 | 222 | // mute 223 | let mutebtn = document.createElement("button"); 224 | mutebtn.textContent = e.muted ? "unmute" : "mute"; 225 | mutebtn.classList.add("elementMuteBtn"); 226 | mutebtn.setAttribute("title", "un/mute element"); 227 | controls.appendChild(mutebtn); 228 | mutebtn.onclick = (evt) => { 229 | browser.tabs.sendMessage(tab.id, { 230 | cmd: evt.target.textContent, 231 | ids: [e.id], 232 | }); 233 | }; 234 | 235 | // volume 236 | let volumebtn = document.createElement("input"); 237 | volumebtn.setAttribute("type", "range"); 238 | volumebtn.setAttribute("min", "0"); 239 | volumebtn.setAttribute("max", "100"); 240 | volumebtn.setAttribute("step", "1"); 241 | volumebtn.setAttribute("value", e.volume * 100); 242 | volumebtn.classList.add("elementVolumeBtn"); 243 | volumebtn.setAttribute("title", "change volume"); 244 | addDataListToRange([0, 25, 50, 75, 100], volumebtn, controls); 245 | controls.appendChild(volumebtn); 246 | 247 | volumebtn.addEventListener("input", (evt) => { 248 | browser.tabs.sendMessage(tab.id, { 249 | cmd: "volume", 250 | id: e.id, 251 | volume: evt.target.value / 100, 252 | }); 253 | }); 254 | 255 | // play / pause 256 | let playpausebtn = document.createElement("button"); 257 | playpausebtn.textContent = e.playing ? "pause" : "play"; 258 | playpausebtn.classList.add("elementPlayPauseBtn"); 259 | playpausebtn.setAttribute("title", "play/pause element"); 260 | controls.appendChild(playpausebtn); 261 | playpausebtn.onclick = async (evt) => { 262 | browser.tabs.sendMessage(tab.id, { 263 | cmd: evt.target.textContent, 264 | ids: [e.id], 265 | }); 266 | }; 267 | 268 | // currentTime 269 | let currentTimebtn = document.createElement("input"); 270 | currentTimebtn.setAttribute("type", "range"); 271 | currentTimebtn.setAttribute("min", "0"); 272 | currentTimebtn.setAttribute("max", "" + parseInt(e.duration)); 273 | if (e.duration === -1) { 274 | currentTimebtn.setAttribute("disabled", "disabled"); 275 | } 276 | currentTimebtn.setAttribute("value", e.currentTime); 277 | 278 | currentTimebtn.classList.add("elementTimeBtn"); 279 | currentTimebtn.setAttribute("title", "change playback position"); 280 | 281 | addDataListToRange( 282 | [ 283 | 0, 284 | parseInt(e.duration) / 4, 285 | parseInt(e.duration) / 2, 286 | (parseInt(e.duration) / 4) * 3, 287 | parseInt(e.duration), 288 | ], 289 | currentTimebtn, 290 | controls, 291 | ); 292 | 293 | controls.appendChild(currentTimebtn); 294 | currentTimebtn.addEventListener("input", (evt) => { 295 | browser.tabs.sendMessage(tab.id, { 296 | cmd: "currentTime", 297 | id: e.id, 298 | currentTime: parseInt(evt.target.value), 299 | }); 300 | }); 301 | } 302 | } 303 | } catch (e) { 304 | console.error("tab ", tab.index, e); 305 | } 306 | } 307 | tablist.focus(); 308 | } 309 | 310 | (async () => { 311 | const styles = await getFromStorage( 312 | "string", 313 | "styles", 314 | ` 315 | /* The World is your canvas, have fun */ 316 | 317 | body { 318 | padding:0; 319 | margin:0; 320 | background-color:silver; 321 | } 322 | #tabs { 323 | padding:0; 324 | margin:0; 325 | } 326 | .tabDiv { 327 | padding:10px; 328 | } 329 | .tabFocusBtn { 330 | width: 100%; 331 | word-break: break-all; 332 | } 333 | .tabMuteBtn { 334 | width: 25%; 335 | } 336 | .tabPauseBtn { 337 | width: 25%; 338 | } 339 | .sitePauseBtn { 340 | width: 25%; 341 | } 342 | .siteMuteBtn { 343 | width: 25%; 344 | } 345 | .elementDiv { 346 | width: 100%; 347 | height: 150px; 348 | } 349 | .previewImg { 350 | height: 150px; 351 | float:left; 352 | border:5px groove gray; 353 | } 354 | .elementControlsDiv { 355 | position:relative; 356 | border:5px groove gray; 357 | height: 140px; 358 | width: 250px; 359 | float: left; 360 | padding-top:10px; 361 | } 362 | .elementFocusBtn { 363 | width: 70%; 364 | margin-left:15%; 365 | } 366 | .elementMuteBtn { 367 | width: 70%; 368 | margin-left:15%; 369 | } 370 | .elementVolumeBtn { 371 | width: 70%; 372 | margin-left:15%; 373 | } 374 | .elementPlayPauseBtn { 375 | width: 70%; 376 | margin-left:15%; 377 | } 378 | .elementTimeBtn { 379 | width: 70%; 380 | margin-left:15%; 381 | } 382 | `, 383 | ); 384 | 385 | let styleSheet = document.createElement("style"); 386 | styleSheet.innerText = styles; 387 | document.head.appendChild(styleSheet); 388 | 389 | queryTabs(); 390 | 391 | setInterval(async () => { 392 | for (const el of document.querySelectorAll(".elementDiv")) { 393 | const newdata = await browser.tabs.sendMessage( 394 | parseInt(el.getAttribute("tid")), 395 | { cmd: "query", id: el.getAttribute("eid") }, 396 | ); 397 | 398 | el.querySelector(".previewImg").setAttribute("src", newdata.poster); 399 | el.querySelector(".elementVolumeBtn").setAttribute( 400 | "value", 401 | newdata.volume * 100, 402 | ); 403 | el.querySelector(".elementVolumeBtn").setAttribute( 404 | "title", 405 | "Volume: " + newdata.volume * 100, 406 | ); 407 | 408 | el.querySelector(".elementTimeBtn").setAttribute( 409 | "value", 410 | newdata.currentTime, 411 | ); 412 | el.querySelector(".elementTimeBtn").setAttribute( 413 | "title", 414 | "CurrentTime: " + newdata.currentTime, 415 | ); 416 | el.querySelector(".elementMuteBtn").textContent = newdata.muted 417 | ? "unmute" 418 | : "mute"; 419 | el.querySelector(".elementPlayPauseBtn").textContent = newdata.playing 420 | ? "pause" 421 | : "play"; 422 | } 423 | }, 150); 424 | })(); 425 | 426 | function isElementInViewport(el) { 427 | var rect = el.getBoundingClientRect(); 428 | 429 | return ( 430 | rect.top >= 0 && 431 | rect.left >= 0 && 432 | rect.bottom <= 433 | (window.innerHeight || document.documentElement.clientHeight) && 434 | rect.right <= (window.innerWidth || document.documentElement.clientWidth) 435 | ); 436 | } 437 | --------------------------------------------------------------------------------