├── .gitignore ├── LICENSE ├── README.md ├── metadata.json ├── package-lock.json ├── package.json ├── rollup.config.js ├── schemas └── net.evermiss.mymindstorm.volume-mixer.gschema.xml └── src ├── applicationStreamSlider.js ├── extension.js ├── prefs.js ├── volumeMixerAddFilterDialog.js ├── volumeMixerPopupMenu.js └── volumeMixerPrefsPage.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | @types 3 | build 4 | dist 5 | .vscode 6 | schemas/gschemas.compiled 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2022 Brendan Early 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gnome Application Volume Mixer 2 | 3 | 4 | 5 | Gnome extension that adds volume sliders for every application emitting audio in the system menu. 6 | 7 | ### Install 8 | 9 | [Gnome Extensions](https://extensions.gnome.org/extension/3499/application-volume-mixer/) 10 | 11 | #### Building Manually 12 | 13 | ```bash 14 | npm i 15 | npm run build 16 | gnome-extensions install dist/volume-mixer.zip 17 | ``` 18 | -------------------------------------------------------------------------------- /metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "shell-version": ["42"], 3 | "uuid": "volume-mixer@evermiss.net", 4 | "orginal-author": "mymindstorm@evermiss.net", 5 | "name": "Application Volume Mixer", 6 | "description": "Control volume output per-application", 7 | "url": "https://github.com/mymindstorm/gnome-volume-mixer" 8 | } 9 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gnome-volume-mixer", 3 | "version": "1.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "gnome-volume-mixer", 9 | "version": "1.0.0", 10 | "license": "MIT", 11 | "devDependencies": { 12 | "@rollup/plugin-commonjs": "^15.0.0", 13 | "rollup": "^2.26.6" 14 | } 15 | }, 16 | "node_modules/@rollup/plugin-commonjs": { 17 | "version": "15.1.0", 18 | "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-15.1.0.tgz", 19 | "integrity": "sha512-xCQqz4z/o0h2syQ7d9LskIMvBSH4PX5PjYdpSSvgS+pQik3WahkQVNWg3D8XJeYjZoVWnIUQYDghuEMRGrmQYQ==", 20 | "dev": true, 21 | "dependencies": { 22 | "@rollup/pluginutils": "^3.1.0", 23 | "commondir": "^1.0.1", 24 | "estree-walker": "^2.0.1", 25 | "glob": "^7.1.6", 26 | "is-reference": "^1.2.1", 27 | "magic-string": "^0.25.7", 28 | "resolve": "^1.17.0" 29 | }, 30 | "engines": { 31 | "node": ">= 8.0.0" 32 | }, 33 | "peerDependencies": { 34 | "rollup": "^2.22.0" 35 | } 36 | }, 37 | "node_modules/@rollup/pluginutils": { 38 | "version": "3.1.0", 39 | "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", 40 | "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", 41 | "dev": true, 42 | "dependencies": { 43 | "@types/estree": "0.0.39", 44 | "estree-walker": "^1.0.1", 45 | "picomatch": "^2.2.2" 46 | }, 47 | "engines": { 48 | "node": ">= 8.0.0" 49 | }, 50 | "peerDependencies": { 51 | "rollup": "^1.20.0||^2.0.0" 52 | } 53 | }, 54 | "node_modules/@rollup/pluginutils/node_modules/estree-walker": { 55 | "version": "1.0.1", 56 | "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", 57 | "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", 58 | "dev": true 59 | }, 60 | "node_modules/@types/estree": { 61 | "version": "0.0.39", 62 | "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", 63 | "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", 64 | "dev": true 65 | }, 66 | "node_modules/balanced-match": { 67 | "version": "1.0.2", 68 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 69 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", 70 | "dev": true 71 | }, 72 | "node_modules/brace-expansion": { 73 | "version": "1.1.11", 74 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 75 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 76 | "dev": true, 77 | "dependencies": { 78 | "balanced-match": "^1.0.0", 79 | "concat-map": "0.0.1" 80 | } 81 | }, 82 | "node_modules/commondir": { 83 | "version": "1.0.1", 84 | "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", 85 | "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", 86 | "dev": true 87 | }, 88 | "node_modules/concat-map": { 89 | "version": "0.0.1", 90 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 91 | "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", 92 | "dev": true 93 | }, 94 | "node_modules/estree-walker": { 95 | "version": "2.0.2", 96 | "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", 97 | "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", 98 | "dev": true 99 | }, 100 | "node_modules/fs.realpath": { 101 | "version": "1.0.0", 102 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 103 | "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", 104 | "dev": true 105 | }, 106 | "node_modules/fsevents": { 107 | "version": "2.3.2", 108 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", 109 | "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", 110 | "dev": true, 111 | "hasInstallScript": true, 112 | "optional": true, 113 | "os": [ 114 | "darwin" 115 | ], 116 | "engines": { 117 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 118 | } 119 | }, 120 | "node_modules/function-bind": { 121 | "version": "1.1.1", 122 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", 123 | "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", 124 | "dev": true 125 | }, 126 | "node_modules/glob": { 127 | "version": "7.2.3", 128 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", 129 | "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", 130 | "dev": true, 131 | "dependencies": { 132 | "fs.realpath": "^1.0.0", 133 | "inflight": "^1.0.4", 134 | "inherits": "2", 135 | "minimatch": "^3.1.1", 136 | "once": "^1.3.0", 137 | "path-is-absolute": "^1.0.0" 138 | }, 139 | "engines": { 140 | "node": "*" 141 | }, 142 | "funding": { 143 | "url": "https://github.com/sponsors/isaacs" 144 | } 145 | }, 146 | "node_modules/has": { 147 | "version": "1.0.3", 148 | "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", 149 | "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", 150 | "dev": true, 151 | "dependencies": { 152 | "function-bind": "^1.1.1" 153 | }, 154 | "engines": { 155 | "node": ">= 0.4.0" 156 | } 157 | }, 158 | "node_modules/inflight": { 159 | "version": "1.0.6", 160 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 161 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 162 | "dev": true, 163 | "dependencies": { 164 | "once": "^1.3.0", 165 | "wrappy": "1" 166 | } 167 | }, 168 | "node_modules/inherits": { 169 | "version": "2.0.4", 170 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 171 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 172 | "dev": true 173 | }, 174 | "node_modules/is-core-module": { 175 | "version": "2.9.0", 176 | "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", 177 | "integrity": "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==", 178 | "dev": true, 179 | "dependencies": { 180 | "has": "^1.0.3" 181 | }, 182 | "funding": { 183 | "url": "https://github.com/sponsors/ljharb" 184 | } 185 | }, 186 | "node_modules/is-reference": { 187 | "version": "1.2.1", 188 | "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", 189 | "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", 190 | "dev": true, 191 | "dependencies": { 192 | "@types/estree": "*" 193 | } 194 | }, 195 | "node_modules/magic-string": { 196 | "version": "0.25.9", 197 | "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", 198 | "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", 199 | "dev": true, 200 | "dependencies": { 201 | "sourcemap-codec": "^1.4.8" 202 | } 203 | }, 204 | "node_modules/minimatch": { 205 | "version": "3.1.2", 206 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", 207 | "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", 208 | "dev": true, 209 | "dependencies": { 210 | "brace-expansion": "^1.1.7" 211 | }, 212 | "engines": { 213 | "node": "*" 214 | } 215 | }, 216 | "node_modules/once": { 217 | "version": "1.4.0", 218 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 219 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 220 | "dev": true, 221 | "dependencies": { 222 | "wrappy": "1" 223 | } 224 | }, 225 | "node_modules/path-is-absolute": { 226 | "version": "1.0.1", 227 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 228 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", 229 | "dev": true, 230 | "engines": { 231 | "node": ">=0.10.0" 232 | } 233 | }, 234 | "node_modules/path-parse": { 235 | "version": "1.0.7", 236 | "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", 237 | "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", 238 | "dev": true 239 | }, 240 | "node_modules/picomatch": { 241 | "version": "2.3.1", 242 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", 243 | "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", 244 | "dev": true, 245 | "engines": { 246 | "node": ">=8.6" 247 | }, 248 | "funding": { 249 | "url": "https://github.com/sponsors/jonschlinkert" 250 | } 251 | }, 252 | "node_modules/resolve": { 253 | "version": "1.22.0", 254 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", 255 | "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==", 256 | "dev": true, 257 | "dependencies": { 258 | "is-core-module": "^2.8.1", 259 | "path-parse": "^1.0.7", 260 | "supports-preserve-symlinks-flag": "^1.0.0" 261 | }, 262 | "bin": { 263 | "resolve": "bin/resolve" 264 | }, 265 | "funding": { 266 | "url": "https://github.com/sponsors/ljharb" 267 | } 268 | }, 269 | "node_modules/rollup": { 270 | "version": "2.75.1", 271 | "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.75.1.tgz", 272 | "integrity": "sha512-zD73rq3Fanr/spmiybMqmGEvOpryj/heLqOb+lubxiXlo8azeJ/z306T2dJYuzfWZPQBS0OT++GXG6Lbd4ToKw==", 273 | "dev": true, 274 | "bin": { 275 | "rollup": "dist/bin/rollup" 276 | }, 277 | "engines": { 278 | "node": ">=10.0.0" 279 | }, 280 | "optionalDependencies": { 281 | "fsevents": "~2.3.2" 282 | } 283 | }, 284 | "node_modules/sourcemap-codec": { 285 | "version": "1.4.8", 286 | "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", 287 | "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", 288 | "dev": true 289 | }, 290 | "node_modules/supports-preserve-symlinks-flag": { 291 | "version": "1.0.0", 292 | "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", 293 | "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", 294 | "dev": true, 295 | "engines": { 296 | "node": ">= 0.4" 297 | }, 298 | "funding": { 299 | "url": "https://github.com/sponsors/ljharb" 300 | } 301 | }, 302 | "node_modules/wrappy": { 303 | "version": "1.0.2", 304 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 305 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", 306 | "dev": true 307 | } 308 | }, 309 | "dependencies": { 310 | "@rollup/plugin-commonjs": { 311 | "version": "15.1.0", 312 | "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-15.1.0.tgz", 313 | "integrity": "sha512-xCQqz4z/o0h2syQ7d9LskIMvBSH4PX5PjYdpSSvgS+pQik3WahkQVNWg3D8XJeYjZoVWnIUQYDghuEMRGrmQYQ==", 314 | "dev": true, 315 | "requires": { 316 | "@rollup/pluginutils": "^3.1.0", 317 | "commondir": "^1.0.1", 318 | "estree-walker": "^2.0.1", 319 | "glob": "^7.1.6", 320 | "is-reference": "^1.2.1", 321 | "magic-string": "^0.25.7", 322 | "resolve": "^1.17.0" 323 | } 324 | }, 325 | "@rollup/pluginutils": { 326 | "version": "3.1.0", 327 | "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", 328 | "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", 329 | "dev": true, 330 | "requires": { 331 | "@types/estree": "0.0.39", 332 | "estree-walker": "^1.0.1", 333 | "picomatch": "^2.2.2" 334 | }, 335 | "dependencies": { 336 | "estree-walker": { 337 | "version": "1.0.1", 338 | "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", 339 | "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", 340 | "dev": true 341 | } 342 | } 343 | }, 344 | "@types/estree": { 345 | "version": "0.0.39", 346 | "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", 347 | "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", 348 | "dev": true 349 | }, 350 | "balanced-match": { 351 | "version": "1.0.2", 352 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 353 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", 354 | "dev": true 355 | }, 356 | "brace-expansion": { 357 | "version": "1.1.11", 358 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 359 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 360 | "dev": true, 361 | "requires": { 362 | "balanced-match": "^1.0.0", 363 | "concat-map": "0.0.1" 364 | } 365 | }, 366 | "commondir": { 367 | "version": "1.0.1", 368 | "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", 369 | "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", 370 | "dev": true 371 | }, 372 | "concat-map": { 373 | "version": "0.0.1", 374 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 375 | "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", 376 | "dev": true 377 | }, 378 | "estree-walker": { 379 | "version": "2.0.2", 380 | "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", 381 | "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", 382 | "dev": true 383 | }, 384 | "fs.realpath": { 385 | "version": "1.0.0", 386 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 387 | "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", 388 | "dev": true 389 | }, 390 | "fsevents": { 391 | "version": "2.3.2", 392 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", 393 | "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", 394 | "dev": true, 395 | "optional": true 396 | }, 397 | "function-bind": { 398 | "version": "1.1.1", 399 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", 400 | "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", 401 | "dev": true 402 | }, 403 | "glob": { 404 | "version": "7.2.3", 405 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", 406 | "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", 407 | "dev": true, 408 | "requires": { 409 | "fs.realpath": "^1.0.0", 410 | "inflight": "^1.0.4", 411 | "inherits": "2", 412 | "minimatch": "^3.1.1", 413 | "once": "^1.3.0", 414 | "path-is-absolute": "^1.0.0" 415 | } 416 | }, 417 | "has": { 418 | "version": "1.0.3", 419 | "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", 420 | "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", 421 | "dev": true, 422 | "requires": { 423 | "function-bind": "^1.1.1" 424 | } 425 | }, 426 | "inflight": { 427 | "version": "1.0.6", 428 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 429 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 430 | "dev": true, 431 | "requires": { 432 | "once": "^1.3.0", 433 | "wrappy": "1" 434 | } 435 | }, 436 | "inherits": { 437 | "version": "2.0.4", 438 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 439 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 440 | "dev": true 441 | }, 442 | "is-core-module": { 443 | "version": "2.9.0", 444 | "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", 445 | "integrity": "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==", 446 | "dev": true, 447 | "requires": { 448 | "has": "^1.0.3" 449 | } 450 | }, 451 | "is-reference": { 452 | "version": "1.2.1", 453 | "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", 454 | "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", 455 | "dev": true, 456 | "requires": { 457 | "@types/estree": "*" 458 | } 459 | }, 460 | "magic-string": { 461 | "version": "0.25.9", 462 | "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", 463 | "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", 464 | "dev": true, 465 | "requires": { 466 | "sourcemap-codec": "^1.4.8" 467 | } 468 | }, 469 | "minimatch": { 470 | "version": "3.1.2", 471 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", 472 | "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", 473 | "dev": true, 474 | "requires": { 475 | "brace-expansion": "^1.1.7" 476 | } 477 | }, 478 | "once": { 479 | "version": "1.4.0", 480 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 481 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 482 | "dev": true, 483 | "requires": { 484 | "wrappy": "1" 485 | } 486 | }, 487 | "path-is-absolute": { 488 | "version": "1.0.1", 489 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 490 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", 491 | "dev": true 492 | }, 493 | "path-parse": { 494 | "version": "1.0.7", 495 | "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", 496 | "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", 497 | "dev": true 498 | }, 499 | "picomatch": { 500 | "version": "2.3.1", 501 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", 502 | "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", 503 | "dev": true 504 | }, 505 | "resolve": { 506 | "version": "1.22.0", 507 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", 508 | "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==", 509 | "dev": true, 510 | "requires": { 511 | "is-core-module": "^2.8.1", 512 | "path-parse": "^1.0.7", 513 | "supports-preserve-symlinks-flag": "^1.0.0" 514 | } 515 | }, 516 | "rollup": { 517 | "version": "2.75.1", 518 | "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.75.1.tgz", 519 | "integrity": "sha512-zD73rq3Fanr/spmiybMqmGEvOpryj/heLqOb+lubxiXlo8azeJ/z306T2dJYuzfWZPQBS0OT++GXG6Lbd4ToKw==", 520 | "dev": true, 521 | "requires": { 522 | "fsevents": "~2.3.2" 523 | } 524 | }, 525 | "sourcemap-codec": { 526 | "version": "1.4.8", 527 | "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", 528 | "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", 529 | "dev": true 530 | }, 531 | "supports-preserve-symlinks-flag": { 532 | "version": "1.0.0", 533 | "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", 534 | "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", 535 | "dev": true 536 | }, 537 | "wrappy": { 538 | "version": "1.0.2", 539 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 540 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", 541 | "dev": true 542 | } 543 | } 544 | } 545 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gnome-volume-mixer", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "rm -rf dist && mkdir -p dist && glib-compile-schemas schemas && cp -r schemas metadata.json dist && rollup -c ./rollup.config.js && cd dist && zip -r -p volume-mixer * " 8 | }, 9 | "author": "", 10 | "license": "MIT", 11 | "devDependencies": { 12 | "@rollup/plugin-commonjs": "^15.0.0", 13 | "rollup": "^2.26.6" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import commonjs from '@rollup/plugin-commonjs'; 2 | 3 | const prefsFooter = [ 4 | 'var init = prefs.init;', 5 | 'var fillPreferencesWindow = prefs.fillPreferencesWindow;', 6 | ].join('\n') 7 | 8 | export default [ 9 | { 10 | input: 'src/extension.js', 11 | output: { 12 | file: `dist/extension.js`, 13 | format: 'iife', 14 | name: 'init', 15 | exports: 'default', 16 | }, 17 | plugins: [ 18 | commonjs() 19 | ], 20 | }, 21 | { 22 | input: 'src/prefs.js', 23 | output: { 24 | file: `dist/prefs.js`, 25 | format: 'iife', 26 | name: 'prefs', 27 | exports: 'default', 28 | footer: prefsFooter, 29 | }, 30 | plugins: [ 31 | commonjs() 32 | ], 33 | }, 34 | ]; 35 | -------------------------------------------------------------------------------- /schemas/net.evermiss.mymindstorm.volume-mixer.gschema.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | [] 6 | 7 | 8 | false 9 | 10 | 11 | true 12 | 13 | 14 | 15 | 16 | 17 | 18 | 'block' 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/applicationStreamSlider.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { BoxLayout, Label } = imports.gi.St; 4 | 5 | // https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/js/ui/status/volume.js 6 | const Volume = imports.ui.status.volume; 7 | 8 | export class ApplicationStreamSlider extends Volume.StreamSlider { 9 | constructor(stream, opts) { 10 | super(Volume.getMixerControl()); 11 | 12 | this.stream = stream; 13 | 14 | if (opts.showIcon) { 15 | this._icon.icon_name = stream.get_icon_name(); 16 | } 17 | 18 | let name = stream.get_name(); 19 | let description = stream.get_description(); 20 | 21 | if (name || description) { 22 | this._vbox = new BoxLayout() 23 | this._vbox.vertical = true; 24 | 25 | this._label = new Label(); 26 | this._label.text = name && opts.showDesc ? `${name} - ${description}` : (name || description); 27 | this._vbox.add(this._label); 28 | 29 | this.item.remove_child(this._slider); 30 | this._vbox.add(this._slider); 31 | this._slider.set_height(32); 32 | 33 | this.item.actor.add(this._vbox); 34 | } 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /src/extension.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { VolumeMixerPopupMenu } from "./volumeMixerPopupMenu"; 4 | 5 | const Main = imports.ui.main; 6 | 7 | var volumeMixer = null; 8 | 9 | function enable() { 10 | volumeMixer = new VolumeMixerPopupMenu(); 11 | 12 | Main.panel.statusArea.aggregateMenu._volume.menu.addMenuItem(volumeMixer); 13 | } 14 | 15 | function disable() { 16 | // REMINDER: It's required for extensions to clean up after themselves when 17 | // they are disabled. This is required for approval during review! 18 | if (volumeMixer !== null) { 19 | volumeMixer.destroy(); 20 | volumeMixer = null; 21 | } 22 | } 23 | 24 | export default function() { 25 | return { 26 | enable, 27 | disable 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/prefs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { VolumeMixerPrefsPage } from './volumeMixerPrefsPage'; 4 | 5 | function init() { 6 | } 7 | 8 | function fillPreferencesWindow(window) { 9 | window.add(new VolumeMixerPrefsPage()); 10 | } 11 | 12 | export default { 13 | init, 14 | fillPreferencesWindow 15 | } 16 | -------------------------------------------------------------------------------- /src/volumeMixerAddFilterDialog.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { Gtk, GObject } = imports.gi; 4 | 5 | export const VolumeMixerAddFilterDialog = GObject.registerClass({ 6 | GTypeName: 'VolumeMixerAddFilterDialog', 7 | }, class VolumeMixerAddFilterDialog extends Gtk.Dialog { 8 | appNameEntry; 9 | filterListData; 10 | 11 | constructor(callingWidget, filterListData) { 12 | super({ 13 | use_header_bar: true, 14 | transient_for: callingWidget.get_root(), 15 | destroy_with_parent: true, 16 | modal: true, 17 | resizable: false, 18 | title: "Add Filtered Application" 19 | }); 20 | 21 | this.filterListData = filterListData; 22 | 23 | const addButton = this.add_button("Add", Gtk.ResponseType.OK); 24 | addButton.get_style_context().add_class('suggested-action'); 25 | addButton.sensitive = false; 26 | this.add_button("Cancel", Gtk.ResponseType.CANCEL); 27 | 28 | const dialogContent = this.get_content_area(); 29 | dialogContent.margin_top = 20 30 | dialogContent.margin_bottom = 20 31 | dialogContent.margin_end = 20 32 | dialogContent.margin_start = 20 33 | 34 | const appNameLabel = new Gtk.Label({ 35 | label: "Application name", 36 | halign: Gtk.Align.START, 37 | margin_bottom: 10 38 | }); 39 | dialogContent.append(appNameLabel); 40 | 41 | this.appNameEntry = new Gtk.Entry(); 42 | this.appNameEntry.connect('activate', () => { 43 | if (this.checkInputValid()) { 44 | this.response(Gtk.ResponseType.OK) 45 | } 46 | }) 47 | dialogContent.append(this.appNameEntry); 48 | 49 | this.appNameEntry.connect("changed", () => { 50 | addButton.sensitive = this.checkInputValid(); 51 | }); 52 | } 53 | 54 | checkInputValid() { 55 | if (this.appNameEntry.text.length === 0) { 56 | return false; 57 | } else if (this.filterListData.indexOf(this.appNameEntry.text) !== -1) { 58 | return false; 59 | } else { 60 | return true; 61 | } 62 | } 63 | }); 64 | -------------------------------------------------------------------------------- /src/volumeMixerPopupMenu.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { ApplicationStreamSlider } from "./applicationStreamSlider"; 4 | 5 | const { Settings, SettingsSchemaSource } = imports.gi.Gio; 6 | const { MixerSinkInput } = imports.gi.Gvc; 7 | 8 | // https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/js/ui/popupMenu.js 9 | const PopupMenu = imports.ui.popupMenu; 10 | // https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/js/ui/status/volume.js 11 | const Volume = imports.ui.status.volume; 12 | const ExtensionUtils = imports.misc.extensionUtils; 13 | const Me = ExtensionUtils.getCurrentExtension(); 14 | 15 | export class VolumeMixerPopupMenu extends PopupMenu.PopupMenuSection { 16 | constructor() { 17 | super(); 18 | this._applicationStreams = {}; 19 | 20 | // The PopupSeparatorMenuItem needs something above and below it or it won't display 21 | this._hiddenItem = new PopupMenu.PopupBaseMenuItem(); 22 | this._hiddenItem.set_height(0) 23 | this.addMenuItem(this._hiddenItem); 24 | 25 | this.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); 26 | 27 | this._control = Volume.getMixerControl(); 28 | this._streamAddedEventId = this._control.connect("stream-added", this._streamAdded.bind(this)); 29 | this._streamRemovedEventId = this._control.connect("stream-removed", this._streamRemoved.bind(this)); 30 | 31 | let gschema = SettingsSchemaSource.new_from_directory( 32 | Me.dir.get_child('schemas').get_path(), 33 | SettingsSchemaSource.get_default(), 34 | false 35 | ); 36 | 37 | this.settings = new Settings({ 38 | settings_schema: gschema.lookup('net.evermiss.mymindstorm.volume-mixer', true) 39 | }); 40 | 41 | this._settingsChangedId = this.settings.connect('changed', () => this._updateStreams()); 42 | 43 | this._updateStreams(); 44 | } 45 | 46 | _streamAdded(control, id) { 47 | if (id in this._applicationStreams) { 48 | return; 49 | } 50 | 51 | const stream = control.lookup_stream_id(id); 52 | 53 | if (stream.is_event_stream || !(stream instanceof MixerSinkInput)) { 54 | return; 55 | } 56 | 57 | if (this._filterMode === "block") { 58 | if (this._filteredApps.indexOf(stream.get_name()) !== -1) { 59 | return; 60 | } 61 | } else if (this._filterMode === "allow") { 62 | if (this._filteredApps.indexOf(stream.get_name()) === -1) { 63 | return; 64 | } 65 | } 66 | 67 | this._applicationStreams[id] = new ApplicationStreamSlider(stream, { showDesc: this._showStreamDesc, showIcon: this._showStreamIcon }); 68 | this.addMenuItem(this._applicationStreams[id].item); 69 | } 70 | 71 | _streamRemoved(_control, id) { 72 | if (id in this._applicationStreams) { 73 | this._applicationStreams[id].item.destroy(); 74 | delete this._applicationStreams[id]; 75 | } 76 | } 77 | 78 | _updateStreams() { 79 | for (const id in this._applicationStreams) { 80 | this._applicationStreams[id].item.destroy(); 81 | delete this._applicationStreams[id]; 82 | } 83 | 84 | this._filteredApps = this.settings.get_strv("filtered-apps"); 85 | this._filterMode = this.settings.get_string("filter-mode"); 86 | this._showStreamDesc = this.settings.get_boolean("show-description"); 87 | this._showStreamIcon = this.settings.get_boolean("show-icon"); 88 | 89 | for (const stream of this._control.get_streams()) { 90 | this._streamAdded(this._control, stream.get_id()) 91 | } 92 | } 93 | 94 | destroy() { 95 | this._control.disconnect(this._streamAddedEventId); 96 | this._control.disconnect(this._streamRemovedEventId); 97 | this.settings.disconnect(this._settingsChangedId); 98 | super.destroy(); 99 | } 100 | }; 101 | -------------------------------------------------------------------------------- /src/volumeMixerPrefsPage.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { VolumeMixerAddFilterDialog } from "./volumeMixerAddFilterDialog"; 4 | 5 | const { Adw, Gio, Gtk, GObject } = imports.gi; 6 | const ExtensionUtils = imports.misc.extensionUtils; 7 | 8 | export const VolumeMixerPrefsPage = GObject.registerClass({ 9 | GTypeName: 'VolumeMixerPrefsPage', 10 | }, class VolumeMixerPrefsPage extends Adw.PreferencesPage { 11 | filterListData = []; 12 | filteredAppsGroup; 13 | settings; 14 | addFilteredAppButtonRow; 15 | 16 | constructor() { 17 | // TODO: Move most of this into a .ui file. 18 | super(); 19 | 20 | this.settings = ExtensionUtils.getSettings("net.evermiss.mymindstorm.volume-mixer"); 21 | this.filterListData = this.settings.get_strv("filtered-apps"); 22 | 23 | // Group for general settings 24 | const generalGroup = new Adw.PreferencesGroup(); 25 | this.add(generalGroup); 26 | 27 | // show-description 28 | const showDescRow = new Adw.ActionRow({ title: 'Show Audio Stream Description' }); 29 | generalGroup.add(showDescRow); 30 | 31 | const showDescToggle = new Gtk.Switch({ 32 | active: this.settings.get_boolean('show-description'), 33 | valign: Gtk.Align.CENTER, 34 | }); 35 | this.settings.bind( 36 | 'show-description', 37 | showDescToggle, 38 | 'active', 39 | Gio.SettingsBindFlags.DEFAULT 40 | ); 41 | 42 | showDescRow.add_suffix(showDescToggle); 43 | showDescRow.activatable_widget = showDescToggle; 44 | 45 | // show-icon 46 | const showIconRow = new Adw.ActionRow({ title: 'Show Application Icon' }); 47 | generalGroup.add(showIconRow); 48 | 49 | const showIconToggle = new Gtk.Switch({ 50 | active: this.settings.get_boolean('show-icon'), 51 | valign: Gtk.Align.CENTER 52 | }); 53 | this.settings.bind( 54 | 'show-icon', 55 | showIconToggle, 56 | 'active', 57 | Gio.SettingsBindFlags.DEFAULT 58 | ); 59 | 60 | showIconRow.add_suffix(showIconToggle); 61 | showIconRow.activatable_widget = showIconToggle; 62 | 63 | // Application filter settings group 64 | const filterGroup = new Adw.PreferencesGroup({ 65 | title: 'Application Filtering', 66 | description: 'Hide applications from the volume mixer.' 67 | }); 68 | this.add(filterGroup); 69 | 70 | // filter-mode 71 | const filterModeModel = new Gio.ListStore({ item_type: FilterMode }); 72 | filterModeModel.append(new FilterMode('Block', 'block')); 73 | filterModeModel.append(new FilterMode('Allow', 'allow')); 74 | 75 | const findCurrentFilterMode = () => { 76 | for (let i = 0; i < filterModeModel.get_n_items(); i++) { 77 | if (filterModeModel.get_item(i).value === this.settings.get_string('filter-mode')) { 78 | return i; 79 | } 80 | } 81 | return -1; 82 | } 83 | 84 | const filterModeRow = new Adw.ComboRow({ 85 | title: 'Filter Mode', 86 | model: filterModeModel, 87 | expression: new Gtk.PropertyExpression(FilterMode, null, 'name'), 88 | selected: findCurrentFilterMode() 89 | }); 90 | filterGroup.add(filterModeRow); 91 | 92 | filterModeRow.connect('notify::selected', () => { 93 | this.settings.set_string('filter-mode', filterModeRow.selectedItem.value); 94 | }); 95 | 96 | // group to act as spacer for filter list 97 | this.filteredAppsGroup = new Adw.PreferencesGroup(); 98 | this.add(this.filteredAppsGroup); 99 | 100 | // List of filtered apps 101 | for (const filteredAppName of this.filterListData) { 102 | this.filteredAppsGroup.add(this.buildFilterListRow(filteredAppName)) 103 | } 104 | 105 | // Add filter entry button 106 | this.createAddFilteredAppButtonRow(); 107 | 108 | // TODO: modes 109 | // - group by application 110 | // - group by application but as a dropdown with streams 111 | // - show all streams 112 | // TODO: go thru github issues 113 | // popularity: page 26, 5th from the top 114 | // TODO: style 115 | } 116 | 117 | createAddFilteredAppButtonRow() { 118 | // I wanted to use Adw.PrefrencesRow, but you can't get the 'row-activated' signal unless it's part of a Gtk.ListBox. 119 | // Adw.PrefrencesGroup doesn't extend Gtk.ListBox. 120 | // TODO: Learn a less hacky to do this. I'm currently too new to GTK to know the best practice. 121 | this.addFilteredAppButtonRow = new Adw.ActionRow(); 122 | const addIcon = Gtk.Image.new_from_icon_name("list-add"); 123 | addIcon.height_request = 40 124 | this.addFilteredAppButtonRow.set_child(addIcon); 125 | this.filteredAppsGroup.add(this.addFilteredAppButtonRow); 126 | // It won't send 'activated' signal w/o this being set. 127 | this.addFilteredAppButtonRow.activatable_widget = addIcon; 128 | this.addFilteredAppButtonRow.connect('activated', (callingWidget) => { 129 | this.showFilteredAppDialog(callingWidget, this.filterListData) 130 | }); 131 | } 132 | 133 | buildFilterListRow(filteredAppName) { 134 | const filterListRow = new Adw.PreferencesRow({ 135 | title: filteredAppName, 136 | activatable: false, 137 | }); 138 | 139 | // Make box for custom row 140 | const filterListBox = new Gtk.Box({ 141 | margin_bottom:6, 142 | margin_top: 6, 143 | margin_end: 15, 144 | margin_start: 15 145 | }); 146 | 147 | // Add title 148 | const filterListLabel = Gtk.Label.new(filterListRow.title); 149 | filterListLabel.hexpand = true; 150 | filterListLabel.halign = Gtk.Align.START 151 | filterListBox.append(filterListLabel); 152 | 153 | // Add remove button 154 | const filterListButton = new Gtk.Button({ 155 | halign: Gtk.Align.END 156 | }); 157 | 158 | // Add icon to remove button 159 | const filterListImage = Gtk.Image.new_from_icon_name("user-trash-symbolic"); 160 | filterListButton.set_child(filterListImage); 161 | 162 | // Tie action to remove button 163 | filterListButton.connect("clicked", (_button) => this.removeFilteredApp(filteredAppName, filterListRow)); 164 | 165 | filterListBox.append(filterListButton); 166 | filterListRow.set_child(filterListBox); 167 | 168 | return filterListRow 169 | } 170 | 171 | removeFilteredApp(filteredAppName, filterListRow) { 172 | this.filterListData.splice(this.filterListData.indexOf(filteredAppName), 1); 173 | this.settings.set_strv("filtered-apps", this.filterListData); 174 | this.filteredAppsGroup.remove(filterListRow); 175 | } 176 | 177 | addFilteredApp(filteredAppName) { 178 | this.filterListData.push(filteredAppName); 179 | this.settings.set_strv("filtered-apps", this.filterListData); 180 | this.filteredAppsGroup.remove(this.addFilteredAppButtonRow); 181 | this.filteredAppsGroup.add(this.buildFilterListRow(filteredAppName)); 182 | this.filteredAppsGroup.add(this.addFilteredAppButtonRow); 183 | } 184 | 185 | showFilteredAppDialog(callingWidget, filterListData) { 186 | const dialog = new VolumeMixerAddFilterDialog(callingWidget, filterListData); 187 | dialog.connect('response', (_dialog, response) => { 188 | if (response === Gtk.ResponseType.OK) { 189 | this.addFilteredApp(dialog.appNameEntry.text); 190 | } 191 | dialog.close(); 192 | dialog.destroy(); 193 | }); 194 | dialog.show(); 195 | } 196 | }); 197 | 198 | const FilterMode = GObject.registerClass({ 199 | Properties: { 200 | 'name': GObject.ParamSpec.string( 201 | 'name', 'name', 'name', 202 | GObject.ParamFlags.READWRITE, 203 | null), 204 | 'value': GObject.ParamSpec.string( 205 | 'value', 'value', 'value', 206 | GObject.ParamFlags.READWRITE, 207 | null), 208 | }, 209 | }, class FilterMode extends GObject.Object { 210 | _init(name, value) { 211 | super._init({ name, value }); 212 | } 213 | }); 214 | --------------------------------------------------------------------------------