├── screenshot.png
├── extension
├── icons
│ └── flowy.png
├── popup.html
├── manifest.json
├── popup.css
├── background.js
└── popup.js
├── README.md
├── LICENSE.md
└── .github
└── workflows
└── main.yml
/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vzze/volume-control/HEAD/screenshot.png
--------------------------------------------------------------------------------
/extension/icons/flowy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vzze/volume-control/HEAD/extension/icons/flowy.png
--------------------------------------------------------------------------------
/extension/popup.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Volume Control
2 |
3 | A [Firefox extension](https://addons.mozilla.org/en-US/firefox/addon/volume-control-tabs/) that allows you to control the volume of each tab from the extension's interface.
4 |
5 |
6 |
7 |
8 |
9 | #### Special thanks to [@pandastic](https://github.com/bandastic) and [@Davilarek](https://github.com/Davilarek)
10 |
--------------------------------------------------------------------------------
/extension/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "background": {
3 | "scripts": ["background.js"]
4 | },
5 |
6 | "manifest_version": 2,
7 | "name": "Volume Control",
8 | "version": "2.7",
9 |
10 | "description": "Control the volume of each tab.",
11 |
12 | "browser_specific_settings": {
13 | "gecko": {
14 | "id": "volume@vzze"
15 | }
16 | },
17 |
18 | "icons": {
19 | "48": "icons/flowy.png"
20 | },
21 |
22 | "browser_action": {
23 | "default_icon": "icons/flowy.png",
24 | "default_title": "Volume",
25 | "default_area": "navbar",
26 | "browser_style": true,
27 | "default_popup": "popup.html"
28 | },
29 |
30 | "permissions": [
31 | "tabs",
32 | "",
33 | "storage"
34 | ]
35 | }
36 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 vzze
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
10 |
--------------------------------------------------------------------------------
/extension/popup.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 5px;
3 | padding: 0px;
4 |
5 | background: black;
6 |
7 | font-family: "Monofur", monospace;
8 | font-size: 14px;
9 |
10 | color: #FFFFFF;
11 | }
12 |
13 | .button {
14 | border: none;
15 | background: none;
16 | text-align: right;
17 |
18 | font-family: "Monofur", monospace;
19 | font-size: 14px;
20 |
21 | transition: all 500ms;
22 |
23 | color: #d3d3d3
24 | }
25 |
26 | .button:hover {
27 | color: #FFFFFF;
28 | }
29 |
30 | ul li {
31 | list-style: none;
32 |
33 | margin-left: 0px;
34 | padding-left: 0px;
35 | margin-right: 57px;
36 | }
37 |
38 | input[type=range] {
39 | -webkit-appearance: none;
40 | appearance: initial;
41 |
42 | background: none;
43 | }
44 |
45 | input[type=range]:focus {
46 | outline: none;
47 | }
48 |
49 | .Slider {
50 | margin: 10px 0 0 0;
51 | }
52 |
53 | .SliderRange {
54 | margin-left: 10px;
55 | float: right;
56 | }
57 |
58 | .SliderValue {
59 | display: inline-block;
60 | position: absolute;
61 |
62 | width: 100;
63 | right: 20px;
64 |
65 | color: 0;
66 | line-height: 20px;
67 | border-radius: 3px;
68 |
69 | padding: 0px 10px;
70 | margin-left: 8px;
71 | }
72 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: Make ZIP CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | paths:
8 | - .github/workflows/main.yml
9 | - extension/**
10 | - build.sh
11 |
12 | permissions:
13 | contents: write
14 | jobs:
15 | bundle-zip:
16 | runs-on: ubuntu-latest
17 | steps:
18 | - uses: actions/checkout@v3
19 | - name: Install zip
20 | run: sudo apt install zip
21 | - name: Make executable
22 | run: chmod +x ./build.sh
23 | - name: Make zip
24 | run: ./build.sh
25 | - name: Upload Artifact
26 | uses: actions/upload-artifact@v3
27 | with:
28 | name: VolumeControl
29 | path: extension.zip
30 | - name: Set Tag Name
31 | id: set-tag
32 | run: echo "::set-output name=tag_name::$(git rev-parse --short "$GITHUB_SHA")"
33 | # - uses: EndBug/latest-tag@latest
34 | # id: "tag_create"
35 | # with:
36 | # tag-name: "devbuild"
37 | - name: Create Dev release
38 | run: |
39 | if ! gh release view devbuild 2>/dev/null; then gh release create devbuild; fi
40 | gh release upload devbuild --clobber extension.zip
41 | env:
42 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
43 | - name: Publish Dev release
44 | run: gh release edit devbuild --title "DevBuild $RELEASE_TAG"
45 | env:
46 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
47 | RELEASE_TAG: ${{ steps.set-tag.outputs.tag_name }}
48 |
--------------------------------------------------------------------------------
/extension/background.js:
--------------------------------------------------------------------------------
1 | const storage = browser.storage.session;
2 | const syncStorage = browser.storage.sync;
3 |
4 | browser.tabs.onRemoved.addListener(async tabId => {
5 | const data = await storage.get("volumes").catch(() => {});
6 |
7 | if(!data) return;
8 | if(!data.volumes) return;
9 | if(data.volumes[tabId] == undefined) return;
10 |
11 | delete data.volumes[tabId];
12 |
13 | await storage.set({ volumes: data.volumes }).catch(() => {});
14 | });
15 |
16 | browser.tabs.onUpdated.addListener(async tabId => {
17 | const data = await storage.get("volumes").catch(() => {});
18 | const sync = await syncStorage.get("nonLinearVolume").catch(() => {});
19 |
20 | if(!data) return;
21 | if(!data.volumes) return;
22 | if(data.volumes[tabId] == undefined) return;
23 |
24 | const nonLinearVolume = (sync.nonLinearVolume) ? true : false;
25 |
26 | const newVolume = (vol) => {
27 | if(nonLinearVolume)
28 | return (vol / 100) ** 2;
29 | else
30 | return (vol / 100);
31 | }
32 |
33 | const res = await browser.tabs.executeScript(tabId, {
34 | code: `document.querySelectorAll("video, audio")[0].volume`
35 | }).catch(() => {});
36 |
37 | if(Math.floor(res[0] * 100) != data.volumes[tabId])
38 | data.volumes[tabId] = Math.floor(res[0] * 100)
39 |
40 | await browser.tabs.executeScript(tabId, {
41 | code: `document.querySelectorAll("video, audio").forEach(elem => elem.volume = ${newVolume(data.volumes[tabId])})`
42 | }).catch(() => { data.volumes[tabId] = 100; });
43 |
44 | await storage.set({ volumes: data.volumes }).catch(() => {});
45 | });
46 |
--------------------------------------------------------------------------------
/extension/popup.js:
--------------------------------------------------------------------------------
1 | let volumes = {}
2 | let nonLinearVolume = false;
3 |
4 | const storage = browser.storage.session;
5 | const syncStorage = browser.storage.sync;
6 |
7 | const throttle = (callback, delay) => {
8 | let shouldWait = false;
9 | let waitingArgs = undefined;
10 |
11 | const timeoutFunc = () => {
12 | if(waitingArgs == undefined) {
13 | shouldWait = false;
14 | } else {
15 | callback(...waitingArgs);
16 | waitingArgs = undefined;
17 | setTimeout(timeoutFunc, delay);
18 | }
19 | }
20 |
21 | return (...args) => {
22 | if(shouldWait) {
23 | waitingArgs = args;
24 | } else {
25 | callback(...args);
26 | shouldWait = true;
27 | setTimeout(timeoutFunc, delay);
28 | }
29 | }
30 | }
31 |
32 | const newVolume = (vol) => {
33 | if(nonLinearVolume)
34 | return (vol / 100) ** 2;
35 | else
36 | return (vol / 100);
37 | }
38 |
39 | const updateRange = throttle(async (id, value) => {
40 | await browser.tabs.executeScript(id, {
41 | code: `document.querySelectorAll("video, audio").forEach(elem => elem.volume = ${newVolume(value)})`
42 | }).catch(() => { return; });
43 |
44 | volumes[id] = value;
45 |
46 | await storage.set({ volumes: volumes }).catch(() => {});
47 | }, 50);
48 |
49 | const rangeSlider = () => {
50 | const slider = document.querySelectorAll('.Slider');
51 | const ranges = document.querySelectorAll('.SliderRange');
52 | const value = document.querySelectorAll('.SliderValue');
53 |
54 | slider.forEach(() => {
55 | value.forEach((currentTarget) => {
56 | currentTarget.innerHTML = currentTarget.previousElementSibling.value;
57 | });
58 |
59 | ranges.forEach((range) => {
60 | range.addEventListener('input', ({ currentTarget }) => {
61 | currentTarget.nextElementSibling.innerHTML = currentTarget.value;
62 |
63 | updateRange(Number(currentTarget.id), Number(currentTarget.value));
64 | });
65 | });
66 | });
67 | }
68 |
69 | const createLiEl = async (tabTitle, tabId) => {
70 | const div = document.createElement("div");
71 |
72 | div.className = "Slider";
73 |
74 | const range = document.createElement("input");
75 |
76 | range.className = "SliderRange";
77 | range.value = volumes[tabId];
78 | range.type = "range";
79 | range.id = String(tabId);
80 | range.max = "100";
81 | range.min = '0';
82 | range.step = '1';
83 |
84 | const title = document.createElement("a");
85 |
86 | if(tabTitle.length > 20)
87 | tabTitle = tabTitle.slice(0, 17) + "...";
88 | else
89 | while(tabTitle.length < 20) tabTitle += ".";
90 |
91 | title.text = tabTitle;
92 |
93 | const span = document.createElement("a");
94 |
95 | span.text = String(volumes[tabId]);
96 | span.className = "SliderValue";
97 |
98 | const pause = document.createElement("button");
99 |
100 | pause.id = "btn" + String(tabId);
101 | pause.classList.add("button");
102 |
103 | const is_paused = await browser.tabs.executeScript(tabId, {
104 | code: `
105 | Array.from(document.querySelectorAll("video, audio")).reduce((n, el) => {
106 | if(el.paused != undefined)
107 | if(el.paused == false)
108 | return n + 1;
109 | else
110 | return n;
111 | else
112 | return n;
113 | }, 0)
114 | `
115 | });
116 |
117 | if(is_paused[0]) pause.textContent = "Pause |"
118 | else pause.textContent = "Resume |"
119 |
120 | pause.onclick = async () => {
121 | const btn = document.getElementById("btn" + String(tabId));
122 |
123 | if(btn.textContent == "Pause |") {
124 | await browser.tabs.executeScript(tabId, {
125 | code: `document.querySelectorAll("video, audio").forEach(elem => elem.pause())`
126 | }).catch(() => { return; });
127 | btn.textContent = "Resume |";
128 | } else {
129 | await browser.tabs.executeScript(tabId, {
130 | code: `document.querySelectorAll("video, audio").forEach(elem => elem.play())`
131 | }).catch(() => { return; });
132 | btn.textContent = "Pause |";
133 | }
134 | }
135 |
136 | div.appendChild(pause);
137 | div.appendChild(title);
138 | div.appendChild(range);
139 | div.appendChild(span);
140 |
141 | return div;
142 | }
143 |
144 | const createNonLinearButton = () => {
145 | const btn = document.createElement("button");
146 |
147 | if(nonLinearVolume)
148 | btn.textContent = "Disable non-linear volume.";
149 | else
150 | btn.textContent = "Enable non-linear volume.";
151 |
152 | btn.classList.add("button");
153 | btn.style["font-size"] = "13px";
154 |
155 | btn.onclick = async () => {
156 | nonLinearVolume = !nonLinearVolume;
157 |
158 | if(nonLinearVolume)
159 | btn.textContent = "Disable non-linear volume.";
160 | else
161 | btn.textContent = "Enable non-linear volume.";
162 |
163 | await syncStorage.set({ nonLinearVolume: nonLinearVolume }).catch(() => {});
164 |
165 | const tabs = await browser.tabs.query({}).catch(() => {});
166 |
167 | await Promise.all(tabs.map(async tab => {
168 | if(volumes[Number(tab.id)] == undefined) return;
169 |
170 | await browser.tabs.executeScript(tab.id, {
171 | code: `document.querySelectorAll("video, audio").forEach(elem => elem.volume = ${newVolume(volumes[Number(tab.id)])})`
172 | }).catch(() => { return; });
173 | })).catch(() => {});
174 | }
175 |
176 | return btn;
177 | }
178 |
179 | (async () => {
180 | const data = await storage.get("volumes").catch(() => {});
181 | const sync = await syncStorage.get("nonLinearVolume").catch(() => {});
182 | const main = await browser.windows.getCurrent().catch(() => {});
183 | const tabs = await browser.tabs.query({}).catch(() => {});
184 |
185 | await storage.clear().catch(() => {});
186 |
187 | const list = document.getElementById("tbs");
188 |
189 | if(data.volumes) volumes = data.volumes;
190 | if(sync.nonLinearVolume) nonLinearVolume = sync.nonLinearVolume
191 |
192 | let atLeastOne = false;
193 |
194 | await Promise.all(tabs.map(async tab => {
195 | if(tab.windowId != main.id) return;
196 |
197 | const res = await browser.tabs.executeScript(Number(tab.id), {
198 | code: `document.querySelectorAll("video, audio").length`
199 | }).catch(() => {});
200 |
201 | if(!res || !res[0]) return;
202 |
203 | atLeastOne = true;
204 |
205 | if(volumes[Number(tab.id)] == undefined) volumes[Number(tab.id)] = 100;
206 |
207 | const newLi = document.createElement("li");
208 |
209 | newLi.appendChild(await createLiEl(tab.title, Number(tab.id)));
210 | list.appendChild(newLi);
211 | })).catch(() => {});
212 |
213 | if(!atLeastOne) {
214 | const a = document.createElement("a");
215 |
216 | a.text = "No tabs with audio open.";
217 | a.style.paddingRight = "30px";
218 |
219 | list.appendChild(
220 | document.createElement("li").appendChild(a)
221 | );
222 | }
223 |
224 | const nonLinear = document.getElementById("non-linear");
225 | nonLinear.appendChild(createNonLinearButton());
226 |
227 | await storage.set({ volumes: volumes }).catch(() => {});
228 | await syncStorage.set({ nonLinearVolume: nonLinearVolume }).catch(() => {});
229 |
230 | rangeSlider();
231 | })();
232 |
--------------------------------------------------------------------------------